pandora_ubu
This commit is contained in:
commit
d5fc76b7cd
7 changed files with 1823 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
*.swp
|
||||
*.pyc
|
0
__init__.py
Normal file
0
__init__.py
Normal file
950
config.jsonc
Normal file
950
config.jsonc
Normal file
|
@ -0,0 +1,950 @@
|
|||
/*
|
||||
-------------------------------------------------------------------------------
|
||||
pan.do/ra Configuration
|
||||
-------------------------------------------------------------------------------
|
||||
You can edit this file. Most changes are effective immediately; everything that
|
||||
affects the database layout (adding a new item key, annotation layer etc.) will
|
||||
require a server restart. You may also want to take a look at the configuration
|
||||
examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution.
|
||||
*/
|
||||
{
|
||||
/*
|
||||
"annotations" configures the annotation panel.
|
||||
"showUsers": If true, include list of users in menu, so that
|
||||
annotations by specific users can be turned on and off
|
||||
*/
|
||||
"annotations": {
|
||||
"showUsers": false
|
||||
},
|
||||
/*
|
||||
"cantPlay" specifies the UI for clips or videos that a user is not allowed
|
||||
to play.
|
||||
"icon": Symbol name (see https://oxjs.org/#examples/symbols/live)
|
||||
"link": URL loaded on click (for example "/rights", if "rights" is
|
||||
defined in "sitePages")
|
||||
"text": Text shown on mouseover
|
||||
*/
|
||||
"cantPlay": {
|
||||
"icon": "noCopyright",
|
||||
"link": "",
|
||||
"text": ""
|
||||
},
|
||||
/*
|
||||
"capabilities" are permissions per user level.
|
||||
Depending on the capability, this is either global:
|
||||
{level: true} means a user of that level has the capability
|
||||
or related to the rights levels of items or texts:
|
||||
{level: x} means a user of that level has the capability for items or
|
||||
texts of a rights level up to and including x
|
||||
*/
|
||||
"capabilities": {
|
||||
"canAddItems": {"member": true, "staff": true, "admin": true},
|
||||
"canDownloadVideo": {"guest": 1, "member": 1, "staff": 4, "admin": 4},
|
||||
"canEditAnnotations": {"staff": true, "admin": true},
|
||||
"canEditDocuments": {"staff": true, "admin": true},
|
||||
"canEditEntities": {"staff": true, "admin": true},
|
||||
"canEditEvents": {"staff": true, "admin": true},
|
||||
"canEditFeaturedEdits": {"staff": true, "admin": true},
|
||||
"canEditFeaturedLists": {"staff": true, "admin": true},
|
||||
"canEditFeaturedTexts": {"staff": true, "admin": true},
|
||||
"canEditMedia": {"staff": true, "admin": true},
|
||||
"canEditMetadata": {"staff": true, "admin": true},
|
||||
"canEditPlaces": {"staff": true, "admin": true},
|
||||
"canEditRightsLevel": {"member": true, "staff": true, "admin": true},
|
||||
"canEditSitePages": {"staff": true, "admin": true},
|
||||
"canEditUsers": {"admin": true},
|
||||
"canExportAnnotations": {"member": true, "staff": true, "admin": true},
|
||||
"canImportAnnotations": {"member": true, "staff": true, "admin": true},
|
||||
"canManageDocuments": {"member": true, "staff": true, "admin": true},
|
||||
"canManageEntities": {"member": true, "staff": true, "admin": true},
|
||||
"canManagePlacesAndEvents": {"member": true, "staff": true, "admin": true},
|
||||
"canManageTitlesAndNames": {"member": true, "staff": true, "admin": true},
|
||||
"canManageUsers": {"staff": true, "admin": true},
|
||||
"canPlayClips": {"guest": 1, "member": 1, "staff": 4, "admin": 4},
|
||||
"canPlayVideo": {"guest": 1, "member": 1, "staff": 4, "admin": 4},
|
||||
"canReadText": {"guest": 0, "member": 0, "staff": 1, "admin": 1},
|
||||
"canRemoveItems": {"admin": true},
|
||||
"canSeeAccessed": {"staff": true, "admin": true},
|
||||
"canSeeDebugMenu": {"staff": true, "admin": true},
|
||||
"canSeeExtraItemViews": {"staff": true, "admin": true},
|
||||
"canSeeMedia": {"staff": true, "admin": true},
|
||||
"canSeeItem": {"guest": 1, "member": 1, "staff": 4, "admin": 4},
|
||||
"canSeeSize": {"staff": true, "admin": true},
|
||||
"canSeeSoftwareVersion": {"staff": true, "admin": true},
|
||||
"canSendMail": {"staff": true, "admin": true}
|
||||
},
|
||||
/*
|
||||
"clipKeys" are the properties that clips can be sorted by (the values are
|
||||
populated automatically). If "sortOperator" is not specified, it will be
|
||||
"+" (ascending) for strings and "-" (descending) for numbers.
|
||||
*/
|
||||
"clipKeys": [
|
||||
{"id": "text", "title": "Text", "type": "string"},
|
||||
{"id": "position", "title": "Position", "type": "float", "sortOperator": "+"},
|
||||
{"id": "duration", "title": "Duration", "type": "float"},
|
||||
{"id": "hue", "title": "Hue", "type": "float", "sortOperator": "+"},
|
||||
{"id": "saturation", "title": "Saturation", "type": "float"},
|
||||
{"id": "lightness", "title": "Lightness", "type": "float"},
|
||||
{"id": "volume", "title": "Volume", "type": "float"}
|
||||
],
|
||||
/*
|
||||
"clipLayers" is the ordered list of public layers that will appear as the
|
||||
text of clips (in grid view, below the icon). Excluding a layer from this
|
||||
list means it will not be included in find annotations.
|
||||
*/
|
||||
"clipLayers": ["publicnotes", "keywords", "subtitles"],
|
||||
/*
|
||||
"entities" can be used to store arbitrary data. They can be referenced in
|
||||
annotations, info view, or elsewhere. Each entry defines a specific class
|
||||
of entity object, its properties and their types (for example an "actor"
|
||||
with "name", "biography", "portrait" etc). The HTML representation for
|
||||
entities can be customized by adding "/static/js/entity.SITENAME.js".
|
||||
Each entity has the following properties:
|
||||
"id": Unique internal ID (can be referenced in "layers")
|
||||
"title": Human-readable title
|
||||
"keys": List of properties
|
||||
"sortType": Optional. If set to "person" or "title", then the sort
|
||||
order can be configured in "Manage Names" or "Manage Titles"
|
||||
Each key has the properties "id", "title" and "type". "type" can be
|
||||
"boolean", "number", "string", "text", "document" (reference to the id of a
|
||||
document added via "Manage Documents") or ["string"] (list of strings).
|
||||
"id" and "name" keys are required. If "alternativeNames" is present, these
|
||||
will act as synonyms when autocompleting values entered as annotations.
|
||||
*/
|
||||
"entities": [
|
||||
{
|
||||
"id": "people",
|
||||
"title": "People",
|
||||
"keys": [
|
||||
{"id": "id", "title": "ID", "type": "string"},
|
||||
{"id": "name", "title": "Name", "type": "string"},
|
||||
{"id": "additionalNames", "title": "Additional Names", "type": ["string"]},
|
||||
{"id": "biography", "title": "Biography", "type": "text"},
|
||||
{"id": "portrait", "title": "Portrait", "type": "document"}
|
||||
],
|
||||
"sortType": "person"
|
||||
}
|
||||
],
|
||||
/*
|
||||
"help" specifies the sections of the help dialog.
|
||||
*/
|
||||
"help": [
|
||||
{"id": "help", "title": "Help"},
|
||||
{"id": "accounts", "title": "Accounts"},
|
||||
{"id": "navigation", "title": "Navigation"},
|
||||
{"id": "views", "title": "Views"},
|
||||
{"id": "timelines", "title": "Timelines"},
|
||||
{"id": "clips", "title": "Clips"},
|
||||
{"id": "maps", "title": "Maps"},
|
||||
{"id": "calendars", "title": "Calendars"},
|
||||
{"id": "find", "title": "Find"},
|
||||
{"id": "filters", "title": "Filters"},
|
||||
{"id": "lists", "title": "Lists"},
|
||||
{"id": "player", "title": "Player"},
|
||||
{"id": "editor", "title": "Editor"},
|
||||
{"id": "documents", "title": "Documents"},
|
||||
{"id": "entities", "title": "Entities"},
|
||||
{"id": "edits", "title": "Edits"},
|
||||
{"id": "texts", "title": "Texts"},
|
||||
{"id": "embeds", "title": "Embeds"}
|
||||
],
|
||||
/*
|
||||
"itemKeys" defines the metadata associated with each item. Required keys
|
||||
are "*", "id" and "title". Annotation layers can be referenced too, which
|
||||
makes them available in the find element. Adding a key with "annotations"
|
||||
as id adds a "find in any annotation layer" option. Further, adding a key
|
||||
with "random" as id can be used for a random sort order.
|
||||
An itemKey must have the following properties:
|
||||
"id": The unique id of the key (as used by the server)
|
||||
"title": The title of the key (as displayed by the client)
|
||||
"type": Can be "boolean", "date", "enum", "float", "hue", "integer",
|
||||
"layer", "string", "text", "time" or ["..."] (list of values of
|
||||
this type). If type is "layer", this is a reference to the
|
||||
annotations layer with the same id.
|
||||
and can have any of the following properties:
|
||||
"additionalSort": Ordered list of {key, operator} objects, where key is
|
||||
another itemKey and operator is "+" or "-". This can be used to
|
||||
override user.ui.listSort when results are sorted by this key.
|
||||
"autocomplete": If true, the find element will provide autocomplete
|
||||
"autocompleteSort": Sort order of autocomplete suggestions
|
||||
"capability": A capability required to see data for this key
|
||||
"columnRequired": If true, the column can't be removed from list view
|
||||
"columnWidth": Default column width in px. If absent, no column for
|
||||
this key can be added in list view.
|
||||
"filter": If true, one can filter results by this key
|
||||
"find": If true, this key will appear as an option in the find element
|
||||
"flag": Can be "country" or "language". If set (and filter is true), a
|
||||
flag icon corresponding to the field's value will be displayed.
|
||||
"format": {type: string, args: [value, value, ...]}, used for special
|
||||
formatting. This will invoke Ox.formatType(args). For details, see
|
||||
https://oxjs.org/#doc/Ox.formatArea etc.
|
||||
"secondaryId": If true, loading the URL "/value" will redirect to the
|
||||
corresponding item, in case there is an exact match for this key
|
||||
"sort": If true, one can sort results by this key
|
||||
"sortOperator": Sort order ("+" or "-"), in case it differs from the
|
||||
default for the key's type ("+" for strings, "-" for numbers)
|
||||
"sortType": Special sort type ("person" or "title") which can be
|
||||
further configured in "Manage Names" or "Manage Titles"
|
||||
"value": {key: string, type: string} or {layer: string, type: string},
|
||||
for keys whose value is derived from other keys or layers (like
|
||||
"number of actors" or "words per minute"). Possible values for type
|
||||
are "length", "lengthperminute", "words", and "wordsperminute".
|
||||
Alternatively, "value" can be set to the string "capability", which
|
||||
results in an itemKey whose boolean value indicates the presence or
|
||||
absence of a userLevel-dependent capability. This can be used to
|
||||
create queries and lists like "all items this user can play" etc.
|
||||
"values": [value, value, ...] Ordered list of values, in case "type" is
|
||||
"enum"
|
||||
*/
|
||||
"itemKeys": [
|
||||
{
|
||||
"id": "*",
|
||||
"title": "All",
|
||||
"type": "text",
|
||||
"find": true
|
||||
},
|
||||
{
|
||||
"id": "title",
|
||||
"title": "Title",
|
||||
"type": "string",
|
||||
"autocomplete": true,
|
||||
"autocompleteSort": [{"key": "timesaccessed", "operator": "-"}],
|
||||
"columnRequired": true,
|
||||
"columnWidth": 180,
|
||||
"find": true,
|
||||
"sort": true,
|
||||
"sortType": "title"
|
||||
},
|
||||
{
|
||||
"id": "name",
|
||||
"title": "Name",
|
||||
"type": ["string"],
|
||||
"autocomplete": true,
|
||||
"find": true
|
||||
},
|
||||
{
|
||||
"id": "director",
|
||||
"title": "Director",
|
||||
"type": ["string"],
|
||||
"autocomplete": true,
|
||||
"columnRequired": true,
|
||||
"columnWidth": 180,
|
||||
"filter": true,
|
||||
"sort": true,
|
||||
"sortType": "person"
|
||||
},
|
||||
{
|
||||
"id": "featuring",
|
||||
"title": "Featuring",
|
||||
"type": ["string"],
|
||||
"autocomplete": true,
|
||||
"columnRequired": true,
|
||||
"columnWidth": 180,
|
||||
"filter": true,
|
||||
"sort": true,
|
||||
"sortType": "person"
|
||||
},
|
||||
{
|
||||
"id": "language",
|
||||
"title": "Language",
|
||||
"type": ["string"],
|
||||
"autocomplete": true,
|
||||
"columnWidth": 120,
|
||||
"filter": true,
|
||||
"find": true,
|
||||
"sort": true
|
||||
},
|
||||
{
|
||||
"id": "country",
|
||||
"title": "Country",
|
||||
"type": "string",
|
||||
"autocomplete": true,
|
||||
"columnWidth": 180,
|
||||
"filter": true,
|
||||
"find": true,
|
||||
"flag": "country",
|
||||
"sort": true
|
||||
},
|
||||
{
|
||||
"id": "year",
|
||||
"title": "Year",
|
||||
"type": "year",
|
||||
"additionalSort": [
|
||||
{"key": "director", "operator": "+"},
|
||||
{"key": "title", "operator": "+"}
|
||||
],
|
||||
"autocomplete": true,
|
||||
"columnWidth": 60,
|
||||
"filter": true,
|
||||
"find": true,
|
||||
"sort": true
|
||||
},
|
||||
{
|
||||
"id": "summary",
|
||||
"title": "Summary",
|
||||
"type": "text",
|
||||
"find": true
|
||||
},
|
||||
{
|
||||
"id": "notes",
|
||||
"title": "Notes",
|
||||
"type": "text",
|
||||
"capability": "canEditMetadata"
|
||||
},
|
||||
{
|
||||
"id": "id",
|
||||
"title": "ID",
|
||||
"type": "string",
|
||||
"columnWidth": 90,
|
||||
"sort": true
|
||||
},
|
||||
{
|
||||
"id": "ubu",
|
||||
"title": "UBU",
|
||||
"type": "string",
|
||||
"columnWidth": 90,
|
||||
"find": true
|
||||
},
|
||||
{
|
||||
"id": "keywords",
|
||||
"title": "Keywords",
|
||||
"type": "layer",
|
||||
"filter": true,
|
||||
"find": true
|
||||
},
|
||||
{
|
||||
"id": "subtitles",
|
||||
"title": "Subtitles",
|
||||
"type": "layer",
|
||||
"find": true
|
||||
},
|
||||
{
|
||||
"id": "publicnotes",
|
||||
"title": "Notes",
|
||||
"type": "layer",
|
||||
"find": true
|
||||
},
|
||||
{
|
||||
"id": "numberofannotations",
|
||||
"title": "Annotations",
|
||||
"type": "integer",
|
||||
"columnWidth": 60,
|
||||
"sort": true
|
||||
},
|
||||
{
|
||||
"id": "duration",
|
||||
"title": "Duration",
|
||||
"type": "time",
|
||||
"columnWidth": 90,
|
||||
"format": {"type": "duration", "args": [0, "short"]},
|
||||
"sort": true
|
||||
},
|
||||
{
|
||||
"id": "resolution",
|
||||
"title": "Resolution",
|
||||
"type": ["integer"],
|
||||
"columnWidth": 90,
|
||||
"format": {"type": "resolution", "args": ["px"]},
|
||||
"sort": true
|
||||
},
|
||||
{
|
||||
"id": "aspectratio",
|
||||
"title": "Aspect Ratio",
|
||||
"type": "float",
|
||||
"columnWidth": 90,
|
||||
"format": {"type": "unit", "args": [":1", 3]},
|
||||
"sort": true
|
||||
},
|
||||
{
|
||||
"id": "pixels",
|
||||
"title": "Pixels",
|
||||
"type": "integer",
|
||||
"columnWidth": 90,
|
||||
"format": {"type": "value", "args": ["px"]},
|
||||
"sort": true
|
||||
},
|
||||
{
|
||||
"id": "hue",
|
||||
"title": "Hue",
|
||||
"type": "hue",
|
||||
"columnWidth": 90,
|
||||
"format": {"type": "color", "args": ["hue"]},
|
||||
"sort": true,
|
||||
"sortOperator": "+"
|
||||
},
|
||||
{
|
||||
"id": "saturation",
|
||||
"title": "Saturation",
|
||||
"type": "float",
|
||||
"columnWidth": 90,
|
||||
"format": {"type": "color", "args": ["saturation"]},
|
||||
"sort": true
|
||||
},
|
||||
{
|
||||
"id": "lightness",
|
||||
"title": "Lightness",
|
||||
"type": "float",
|
||||
"columnWidth": 90,
|
||||
"format": {"type": "color", "args": ["lightness"]},
|
||||
"sort": true
|
||||
},
|
||||
{
|
||||
"id": "volume",
|
||||
"title": "Volume",
|
||||
"type": "float",
|
||||
"columnWidth": 60,
|
||||
"format": {"type": "color", "args": ["lightness"]},
|
||||
"sort": true
|
||||
},
|
||||
{
|
||||
"id": "numberofcuts",
|
||||
"title": "Number of Cuts",
|
||||
"type": "integer",
|
||||
"columnWidth": 60,
|
||||
"format": {"type": "number", "args": []},
|
||||
"sort": true,
|
||||
"value": {"key": "cuts", "type": "length"}
|
||||
},
|
||||
{
|
||||
"id": "cutsperminute",
|
||||
"title": "Cuts per Minute",
|
||||
"type": "float",
|
||||
"columnWidth": 60,
|
||||
"format": {"type": "number", "args": [3]},
|
||||
"sort": true,
|
||||
"value": {"key": "cuts", "type": "lengthperminute"}
|
||||
},
|
||||
{
|
||||
"id": "words",
|
||||
"title": "Number of Words",
|
||||
"type": "integer",
|
||||
"columnWidth": 60,
|
||||
"format": {"type": "number", "args": []},
|
||||
"sort": true,
|
||||
"value": {"layer": "subtitles", "type": "words"}
|
||||
},
|
||||
{
|
||||
"id": "wordsperminute",
|
||||
"title": "Words per Minute",
|
||||
"type": "float",
|
||||
"columnWidth": 60,
|
||||
"format": {"type": "number", "args": [3]},
|
||||
"sort": true,
|
||||
"value": {"layer": "subtitles", "type": "wordsperminute"}
|
||||
},
|
||||
{
|
||||
"id": "size",
|
||||
"title": "Size",
|
||||
"type": "integer",
|
||||
"capability": "canSeeMedia",
|
||||
"columnWidth": 60,
|
||||
"format": {"type": "value", "args": ["B"]},
|
||||
"sort": true
|
||||
},
|
||||
{
|
||||
"id": "bitrate",
|
||||
"title": "Bitrate",
|
||||
"type": "integer",
|
||||
"columnWidth": 60,
|
||||
"format": {"type": "value", "args": ["bps"]},
|
||||
"sort": true
|
||||
},
|
||||
{
|
||||
"id": "numberoffiles",
|
||||
"title": "Number of Files",
|
||||
"type": "integer",
|
||||
"capability": "canSeeMedia",
|
||||
"columnWidth": 60,
|
||||
"sort": true,
|
||||
"value": {"key": "files", "type": "length"}
|
||||
},
|
||||
{
|
||||
"id": "user",
|
||||
"title": "User",
|
||||
"type": "string",
|
||||
"capability": "canSeeMedia",
|
||||
"find": true
|
||||
},
|
||||
{
|
||||
"id": "created",
|
||||
"title": "Date Created",
|
||||
"type": "date",
|
||||
"columnWidth": 120,
|
||||
"format": {"type": "date", "args": ["%Y-%m-%d %H:%M:%S"]},
|
||||
"sort": true
|
||||
},
|
||||
{
|
||||
"id": "modified",
|
||||
"title": "Last Modified",
|
||||
"type": "date",
|
||||
"columnWidth": 90,
|
||||
"format": {"type": "date", "args": ["%Y-%m-%d %H:%M:%S"]},
|
||||
"sort": true
|
||||
},
|
||||
{
|
||||
"id": "accessed",
|
||||
"title": "Last Accessed",
|
||||
"type": "date",
|
||||
"capability": "canSeeAccessed",
|
||||
"columnWidth": 90,
|
||||
"format": {"type": "date", "args": ["%Y-%m-%d %H:%M:%S"]},
|
||||
"sort": true
|
||||
},
|
||||
{
|
||||
"id": "timesaccessed",
|
||||
"title": "Times Accessed",
|
||||
"type": "integer",
|
||||
"capability": "canSeeAccessed",
|
||||
"columnWidth": 60,
|
||||
"format": {"type": "number", "args": []},
|
||||
"sort": true
|
||||
},
|
||||
{
|
||||
"id": "rightslevel",
|
||||
"title": "Rights Level",
|
||||
"type": "enum",
|
||||
"columnWidth": 90,
|
||||
"format": {"type": "ColorLevel", "args": [
|
||||
["Public", "Restricted", "Private"]
|
||||
]},
|
||||
"sort": true,
|
||||
"sortOperator": "+",
|
||||
"values": ["Public", "Restricted", "Private", "Unknown"]
|
||||
},
|
||||
{
|
||||
"id": "random",
|
||||
"title": "Random",
|
||||
"type": "integer",
|
||||
"sort": true
|
||||
}
|
||||
],
|
||||
/*
|
||||
"itemName" specifies the name of items ("Movies", "Videos" or similar).
|
||||
Note that anything excessively long may cause layout errors.
|
||||
*/
|
||||
"itemName": {
|
||||
"singular": "Video",
|
||||
"plural": "Videos"
|
||||
},
|
||||
/*
|
||||
"itemRequiresVideo" regulates if items without video can be created. If set
|
||||
to true, the only way to add a new item is to upload a new video.
|
||||
*/
|
||||
"itemRequiresVideo": true,
|
||||
/*
|
||||
"itemTitleKeys" is a list of itemKeys required to compose the item title
|
||||
displayed at the top of the screen. This title can be customized by adding
|
||||
"/static/js/getItemTitle.SITENAME.js".
|
||||
*/
|
||||
"itemTitleKeys": ["title", "director", "year"],
|
||||
/*
|
||||
"itemViews" is an ordered list of available item views. Implemented views
|
||||
are "info", "documents", "player", "editor", "timeline", "clips", "map",
|
||||
"calendar", "data" and "media".
|
||||
*/
|
||||
"itemViews": [
|
||||
{"id": "info", "title": "Info"},
|
||||
{"id": "player", "title": "Player"},
|
||||
{"id": "editor", "title": "Editor"},
|
||||
{"id": "timeline", "title": "Timeline"},
|
||||
{"id": "clips", "title": "Clips"},
|
||||
{"id": "map", "title": "Map"},
|
||||
{"id": "calendar", "title": "Calendar"},
|
||||
{"id": "documents", "title": "Documents"},
|
||||
{"id": "data", "title": "Data"},
|
||||
{"id": "media", "title": "Media"}
|
||||
],
|
||||
/*
|
||||
"language" is the default language for annotations of type "text". Text in
|
||||
other languages can be added via markup, for example:
|
||||
<span lang="fr">Voilà!</span>
|
||||
*/
|
||||
"language": "en",
|
||||
/*
|
||||
"languages" is the list of languages that the interface can be switched to.
|
||||
Currently, localization exists for "ar", "el", "en" and "hi".
|
||||
*/
|
||||
"languages": ["ar", "el", "en", "hi"],
|
||||
/*
|
||||
"layers" defines the types of time-based annotations that can be entered.
|
||||
Required keys are:
|
||||
"id": Internal ID
|
||||
"item": Name of one such annotation (singular)
|
||||
"title": Display title
|
||||
"type": Can be "entity", "string" or "text" (strings cannot contain
|
||||
line breaks, text can contain HTML markup).
|
||||
Optional keys are:
|
||||
"canAddAnnotations": Permissions per user level
|
||||
"entity": ID of the referenced entity (if type is "entity")
|
||||
"hasEvents": If true, the calendar will be populated with matches from
|
||||
this layer
|
||||
"hasPlaces": If true, the map will be populated with matches from this
|
||||
layer
|
||||
"isSubtitles": If true, this layer will be displayed as subtitles
|
||||
"overlap": If true, overlapping annotations are allowed. Note that
|
||||
enforcement of "overlap": false is not implemented
|
||||
"showInfo": If true, user and creation time will be displayed in the
|
||||
tooltip that appears on mouseover.
|
||||
*/
|
||||
"layers": [
|
||||
{
|
||||
"id": "keywords",
|
||||
"title": "Keywords",
|
||||
"canAddAnnotations": {"member": true, "staff": true, "admin": true},
|
||||
"item": "Keyword",
|
||||
"overlap": true,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"id": "privatenotes",
|
||||
"title": "Private Notes",
|
||||
"canAddAnnotations": {"member": true, "staff": true, "admin": true},
|
||||
"item": "Private Note",
|
||||
"overlap": true,
|
||||
"private": true,
|
||||
"showInfo": true,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"id": "publicnotes",
|
||||
"title": "Notes",
|
||||
"canAddAnnotations": {"member": true, "staff": true, "admin": true},
|
||||
"item": "Public Note",
|
||||
"overlap": true,
|
||||
"showInfo": true,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"id": "subtitles",
|
||||
"title": "Subtitles",
|
||||
"canAddAnnotations": {"staff": true, "admin": true},
|
||||
"hasEvents": true,
|
||||
"hasPlaces": true,
|
||||
"isSubtitles": true,
|
||||
"item": "Subtitle",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
/*
|
||||
"itemViews" is an ordered list of available list views. Implemented views
|
||||
are "list", "grid", "timelines", "clips", "clip", "map" and "calendar".
|
||||
*/
|
||||
"listViews": [
|
||||
{"id": "list", "title": "as List"},
|
||||
{"id": "grid", "title": "as Grid"},
|
||||
{"id": "timelines", "title": "with Timelines"},
|
||||
{"id": "clips", "title": "with Clips"},
|
||||
{"id": "clip", "title": "as Clips"},
|
||||
{"id": "map", "title": "on Map"},
|
||||
{"id": "calendar", "title": "on Calendar"}
|
||||
],
|
||||
/*
|
||||
"media" defines additional media that gets added when importing items.
|
||||
"importPosters": If true, a poster file will be imported (if present)
|
||||
"importFrames": If true, 3 full-resolution frames per file will be
|
||||
imported. This is useful to render high-resolution icons and
|
||||
posters for an instance that has only low-resolution video.
|
||||
*/
|
||||
"media": {
|
||||
"importPosters": false,
|
||||
"importFrames": false
|
||||
},
|
||||
/*
|
||||
"menuExtras" can be used to add extra functionality to the main menu bar.
|
||||
The plug-in architecture is not yet finalized, documentation forthcoming.
|
||||
*/
|
||||
"menuExtras": [
|
||||
"user",
|
||||
// "locale",
|
||||
"reload"
|
||||
],
|
||||
/*
|
||||
"personalLists" specifies which pre-defined lists a new member will have
|
||||
after signup. "title" is required. If "query" is present, this defines a
|
||||
smart list. "query" can be any pan.do/ra query object, see /api/find for
|
||||
further documentation. If you need to reference the username (for example
|
||||
in order to include a default "My Videos" list), you can use the syntax
|
||||
{"user": "{username}"}
|
||||
*/
|
||||
"personalLists": [
|
||||
{"title": "Favorites"}
|
||||
],
|
||||
/*
|
||||
"posters" contains details about the poster icons. "ratio" is the default
|
||||
ratio (used to display a placeholder while poster icons are loading).
|
||||
*/
|
||||
"posters": {
|
||||
"ratio": 0.625
|
||||
},
|
||||
/*
|
||||
"rightsLevel" defines which initial rights level will be assigned to items
|
||||
and texts created by users of these user levels.
|
||||
*/
|
||||
"rightsLevel": {"member": 2, "staff": 2, "admin": 2},
|
||||
/*
|
||||
"rightsLevels" is an ordered list of rights levels, one of which will be
|
||||
assigned to each item.
|
||||
*/
|
||||
"rightsLevels": [
|
||||
{"name": "Public", "color": [128, 255, 128]},
|
||||
{"name": "Restricted", "color": [255, 192, 128]},
|
||||
{"name": "Private", "color": [255, 128, 128]}
|
||||
],
|
||||
/*
|
||||
"site" contains various settings for this instance. In "email", "contact"
|
||||
if the address in the contact form (to), "system" is the address used by
|
||||
the system (from).
|
||||
*/
|
||||
"site": {
|
||||
"description": "This is a demo of pan.do/ra - a free, open source media archive. It allows you to manage large, decentralized collections of video, to collaboratively create metadata and time-based annotations, and to serve your archive as a cutting-edge web application.",
|
||||
"email": {
|
||||
// E-mail address in contact form (to)
|
||||
"contact": "system@pandora.local",
|
||||
"footer": "-- \npan.do/ra - http://pan.do/ra",
|
||||
"prefix": "pan.do/ra News -",
|
||||
// E-mail address uses by the system (from)
|
||||
"system": "system@pandora.local"
|
||||
},
|
||||
"https": false,
|
||||
"id": "ubu",
|
||||
"name": "UBUWeb",
|
||||
"sendReferrer": true,
|
||||
"url": "ubu.0x2620.org"
|
||||
},
|
||||
/*
|
||||
"sitePages" defines the sections of the main site dialog. If "news" is
|
||||
present, this will add an interface to add news items. If "contact" is
|
||||
present, this will add an interface to contact the site owners.
|
||||
*/
|
||||
"sitePages": [
|
||||
{"id": "about", "title": "About"},
|
||||
{"id": "news", "title": "News"},
|
||||
// {"id": "tour", "title": "Take a Tour"},
|
||||
{"id": "faq", "title": "Frequently Asked Questions"},
|
||||
{"id": "terms", "title": "Terms of Service"},
|
||||
{"id": "license", "title": "License"},
|
||||
{"id": "contact", "title": "Contact"}
|
||||
],
|
||||
/*
|
||||
"sites" specifies which other pan.do/ra instances, if any, will appear in
|
||||
the user interface for creating embeds. This allows for easier creation of
|
||||
cross-instance references.
|
||||
*/
|
||||
"sites": [
|
||||
{"name": "0xDB", "url": "0xdb.org", "https": true},
|
||||
{"name": "Pad.ma", "url": "pad.ma", "https": true},
|
||||
{"name": "Indiancine.ma", "url": "indiancine.ma", "https": true}
|
||||
],
|
||||
/*
|
||||
"textRightsLevels" defines a list of rights levels for texts.
|
||||
*/
|
||||
"textRightsLevels": [
|
||||
{"name": "Public", "color": [128, 255, 128]},
|
||||
{"name": "Private", "color": [255, 128, 128]}
|
||||
],
|
||||
/*
|
||||
"themes" is a list of themes that the user interface can be switched to.
|
||||
Currently available themes are "oxlight", "oxmedium" and "oxdark". The
|
||||
default theme can be set in user.ui.theme.
|
||||
*/
|
||||
"themes": ["oxlight", "oxmedium", "oxdark"],
|
||||
/*
|
||||
"timelines" is a list of timeline types. Implemented types are "antialias",
|
||||
"slitscal", "keyframes" and "auto".
|
||||
*/
|
||||
"timelines": [
|
||||
{"id": "antialias", "title": "Anti-Alias"},
|
||||
{"id": "slitscan", "title": "Slit-Scan"},
|
||||
{"id": "keyframes", "title": "Keyframes"},
|
||||
{"id": "audio", "title": "Waveform"}
|
||||
],
|
||||
/*
|
||||
"totals" specifies which totals are displayed in the status bar at the
|
||||
bottom of list views. Possible ids are "duration", "files", "items",
|
||||
"pixels" and "size". Adding a capability limits the display of a specific
|
||||
total to users of the corresponding user levels.
|
||||
*/
|
||||
"totals": [
|
||||
{"id": "items"},
|
||||
{"id": "files", "capability": "canSeeMedia"},
|
||||
{"id": "duration", "capability": "canSeeMedia"},
|
||||
{"id": "size", "capability": "canSeeMedia"},
|
||||
{"id": "pixels"}
|
||||
],
|
||||
/*
|
||||
If "tv" is set to true, then in TV mode, the site logo will be displayed in
|
||||
the corner of the screen.
|
||||
*/
|
||||
"tv": {
|
||||
"showLogo": false
|
||||
},
|
||||
/*
|
||||
The "user" object contains the default user settings. "ui" is the default
|
||||
interface state for new users, and after selecting "Reset UI Settings" in
|
||||
Preferences -> Advanced. This is the place to configure various defaults,
|
||||
like the site-wide language and theme, the default list and item views, the
|
||||
default set of filters, etc.
|
||||
Please make sure that in case you rename or remove entries that are
|
||||
referenced in "ui", you update them here as well.
|
||||
*/
|
||||
"user": {
|
||||
"level": "guest",
|
||||
"ui": {
|
||||
"annotationsCalendarSize": 128,
|
||||
"annotationsMapSize": 128,
|
||||
"annotationsRange": "all",
|
||||
"annotationsSize": 256,
|
||||
"annotationsSort": "position",
|
||||
"calendarFind": "",
|
||||
"calendarSelection": "",
|
||||
"clipColumns": 2,
|
||||
"columns": {
|
||||
"Colors": {
|
||||
"columns": ["title", "director", "language", "hue", "saturation", "brightness"],
|
||||
"columnWidth": {}
|
||||
}
|
||||
},
|
||||
"documents": {},
|
||||
"documentSize": 256,
|
||||
"documentsSelection": {},
|
||||
"documentsSort": [{"key": "name", "operator": "+"}],
|
||||
"documentsView": "grid",
|
||||
"edit": "",
|
||||
"edits": {},
|
||||
"editSelection": [],
|
||||
"editSort": [
|
||||
{"key": "index", "operator": "+"},
|
||||
{"key": "year", "operator": "+"},
|
||||
{"key": "director", "operator": "+"},
|
||||
{"key": "title", "operator": "+"},
|
||||
{"key": "position", "operator": "+"},
|
||||
{"key": "duration", "operator": "+"}
|
||||
],
|
||||
"editView": "list",
|
||||
"embedSize": 256,
|
||||
"entitiesSelection": {},
|
||||
"entitiesType": "",
|
||||
"filters": [
|
||||
{"id": "director", "sort": [{"key": "items", "operator": "-"}]},
|
||||
{"id": "country", "sort": [{"key": "items", "operator": "-"}]},
|
||||
{"id": "year", "sort": [{"key": "name", "operator": "-"}]},
|
||||
{"id": "featuring", "sort": [{"key": "items", "operator": "-"}]},
|
||||
{"id": "keywords", "sort": [{"key": "items", "operator": "-"}]}
|
||||
],
|
||||
"filtersSize": 176,
|
||||
"find": {"conditions": [], "operator": "&"},
|
||||
"followPlayer": true,
|
||||
"help": "",
|
||||
"icons": "posters",
|
||||
"infoIconSize": 256,
|
||||
"item": "",
|
||||
"itemFind": "",
|
||||
"itemSort": [{"key": "position", "operator": "+"}],
|
||||
"itemView": "info",
|
||||
"listColumns": ["title", "director", "year", "language", "duration"],
|
||||
"listColumnWidth": {},
|
||||
"listSelection": [],
|
||||
"listSort": [{"key": "title", "operator": "+"}],
|
||||
"listView": "grid",
|
||||
"lists": {},
|
||||
"locale": "en",
|
||||
"mapFind": "",
|
||||
"mapSelection": "",
|
||||
"page": "",
|
||||
"part": {
|
||||
"api": "",
|
||||
"documents": "",
|
||||
"entities": "",
|
||||
"faq": "",
|
||||
"help": "",
|
||||
"news": "",
|
||||
"preferences": "",
|
||||
"tv": ""
|
||||
},
|
||||
"section": "items",
|
||||
"sequenceMode": "shape",
|
||||
"sequenceSort": [{"key": "director", "operator": "+"}],
|
||||
"showAdvancedEmbedOptions": false,
|
||||
"showAnnotations": true,
|
||||
"showAnnotationsCalendar": true,
|
||||
"showAnnotationsMap": true,
|
||||
"showBrowser": true,
|
||||
"showCalendarControls": false,
|
||||
"showClips": true,
|
||||
"showDocument": true,
|
||||
"showFilters": true,
|
||||
"showIconBrowser": false,
|
||||
"showInfo": true,
|
||||
"showLayers": {
|
||||
"keywords": true,
|
||||
"privatenotes": true,
|
||||
"publicnotes": true,
|
||||
"subtitles": true
|
||||
},
|
||||
"showMapControls": false,
|
||||
"showMapLabels": false,
|
||||
"showFolder": {
|
||||
"edits": {
|
||||
"personal": true,
|
||||
"favorite": true,
|
||||
"featured": true,
|
||||
"volumes": true
|
||||
},
|
||||
"items": {
|
||||
"personal": true,
|
||||
"favorite": true,
|
||||
"featured": true,
|
||||
"volumes": true
|
||||
},
|
||||
"texts": {
|
||||
"personal": true,
|
||||
"favorite": true,
|
||||
"featured": true
|
||||
}
|
||||
},
|
||||
"showReflections": true,
|
||||
"showSidebar": true,
|
||||
"showSitePosters": false,
|
||||
"showTimeline": true,
|
||||
"sidebarSize": 256,
|
||||
"text": "",
|
||||
"texts": {},
|
||||
"theme": "oxmedium",
|
||||
"updateAdvancedFindResults": false,
|
||||
"videoLoop": false,
|
||||
"videoMuted": false,
|
||||
"videoPoints": {},
|
||||
"videoResolution": 480,
|
||||
"videoScale": "fit",
|
||||
"videoSize": "small",
|
||||
"videoSubtitles": true,
|
||||
"videoTimeline": "antialias",
|
||||
"videoView": "player",
|
||||
"videoVolume": 1
|
||||
},
|
||||
"script": "",
|
||||
"username": "",
|
||||
"volumes": []
|
||||
},
|
||||
/*
|
||||
"userLevels" is an ordered list of user classes. The first entry is for
|
||||
unregistered visitors.
|
||||
*/
|
||||
"userLevels": ["guest", "member", "staff", "admin"],
|
||||
/*
|
||||
"video" contains the video settings.
|
||||
"formats": Supported video formats. Should be ["webm", "mp4"], or
|
||||
["webm"] in case iDevices are not part of the audience
|
||||
"previewRatio": Aspect ratio used in the info panel in the bottom left
|
||||
corner of the screen
|
||||
"resolutions": List of video resolutions. Supported values are 96, 144,
|
||||
240, 288, 360, 432, 480, 720 and 1080.
|
||||
"torrent": If true, video downloads are offered via BitTorrent
|
||||
*/
|
||||
"video": {
|
||||
"formats": ["mp4"],
|
||||
"previewRatio": 1.3333333333,
|
||||
"resolutions": [480],
|
||||
"torrent": false
|
||||
}
|
||||
}
|
108
data.py
Normal file
108
data.py
Normal file
|
@ -0,0 +1,108 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
import json
|
||||
import os
|
||||
from os.path import join, normpath, dirname
|
||||
|
||||
import ox
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
import item.models
|
||||
import archive.models
|
||||
|
||||
base = normpath(dirname(__file__))
|
||||
|
||||
with open(join(base, 'ubu.json')) as f:
|
||||
data = json.load(f)
|
||||
|
||||
|
||||
def get_item(ubu):
|
||||
qs = item.models.Item.objects.filter(data__contains='"ubu": "%s"'%ubu)
|
||||
if qs.count():
|
||||
return qs[0], False
|
||||
return item.models.Item(), True
|
||||
|
||||
def update_item(d, add_only=False):
|
||||
info = {{
|
||||
'artist': 'director',
|
||||
'description': 'summary',
|
||||
'id': 'ubu',
|
||||
'mp4': 'stream',
|
||||
}.get(k,k):d[k] for k in d}
|
||||
for k in ('type', 'flv', 'video'):
|
||||
if k in info:
|
||||
del info[k]
|
||||
if 'director' in info:
|
||||
if len(info['director']) > 200:
|
||||
info['case'] = info['director']
|
||||
info['director'] = [info['director'].split('.')[0][:200]]
|
||||
else:
|
||||
info['director'] = [info['director']]
|
||||
if 'summary' in info:
|
||||
info['summary'] = ox.sanitize_html(info['summary'].replace('\n', '<br>\n'))
|
||||
if 'title' in info:
|
||||
info['title'] = info['title'].replace('(%s)' % info.get('year', ''), '').strip()
|
||||
i, created = get_item(info['ubu'])
|
||||
if not created and add_only:
|
||||
return
|
||||
print info['ubu'], info.get('title', 'Untitled')
|
||||
for k in info:
|
||||
i.data[k] = info[k]
|
||||
i.level = 0
|
||||
i.save()
|
||||
#i.make_poster(True)
|
||||
|
||||
def update_items(add_only=False):
|
||||
for d in data:
|
||||
update_item(d, add_only)
|
||||
|
||||
def update_item_stream(i, force=False):
|
||||
if update_stream(i) or force:
|
||||
for s in i.streams():
|
||||
s.make_timeline()
|
||||
i.save()
|
||||
i.update_timeline()
|
||||
i.make_poster(True)
|
||||
return True
|
||||
return False
|
||||
|
||||
def update_stream(i):
|
||||
stream_url = i.data.get('stream')
|
||||
if not stream_url:
|
||||
return
|
||||
for s in i.streams():
|
||||
if s.info['url'] != stream_url:
|
||||
s.delete()
|
||||
else:
|
||||
return False
|
||||
oshash = ox.net.oshash(stream_url)
|
||||
if oshash == 'IOError':
|
||||
print 'invalid url', stream_url
|
||||
return
|
||||
f, created = archive.models.File.objects.get_or_create(oshash=oshash)
|
||||
f.item = i
|
||||
f.selected = True
|
||||
f.available = True
|
||||
f.info['extension'] = 'mp4'
|
||||
f.path = stream_url
|
||||
f.save()
|
||||
s, created = archive.models.Stream.objects.get_or_create(source=None, file=f)
|
||||
if created:
|
||||
s.info['url'] = stream_url
|
||||
s.available = True
|
||||
s.resolution = 480
|
||||
s.format = 'mp4'
|
||||
s.save()
|
||||
return True
|
||||
return False
|
||||
|
||||
def update_streams():
|
||||
import itemlist.models
|
||||
l = itemlist.models.List.get('j:with Video')
|
||||
for i in item.models.Item.objects.all():
|
||||
if i.public_id in ['HJ', 'ANV']:
|
||||
continue
|
||||
print i
|
||||
if update_item_stream(i):
|
||||
l.add(i)
|
25
get_data.py
Normal file
25
get_data.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
import json
|
||||
import re
|
||||
import ox
|
||||
import ox.web.ubu
|
||||
import codecs
|
||||
|
||||
ids = ox.web.ubu.get_ids()
|
||||
|
||||
data = {}
|
||||
for id in ids:
|
||||
info = ox.web.ubu.get_data(id)
|
||||
if 'mp4' in info:
|
||||
if 'description' in info:
|
||||
info['description'] = re.sub(' *\n *', '\n', info['description'])
|
||||
info['description'] = re.sub('\n+', '\n\n', info['description'])
|
||||
if isinstance(info['description'], str):
|
||||
info['description'] = info['description'].decode('latin-1')
|
||||
info['description'] = ox.fix_bad_unicode(info['description'])
|
||||
info['mp4'] = info['mp4'].replace(' ', '%20')
|
||||
data[id] = info
|
||||
else:
|
||||
print info
|
||||
|
||||
with codecs.open('ubu.json', 'w', 'utf-8') as f:
|
||||
json.dump(data.values(), f, indent=2, ensure_ascii=False)
|
85
install.py
Executable file
85
install.py
Executable file
|
@ -0,0 +1,85 @@
|
|||
#!/usr/bin/python2
|
||||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
import os
|
||||
from os.path import join, abspath, basename, dirname
|
||||
import re
|
||||
|
||||
name = 'ubu'
|
||||
base = abspath(dirname(__file__))
|
||||
os.chdir(base)
|
||||
|
||||
for root, folders, files in os.walk(join(base, 'static')):
|
||||
for f in files:
|
||||
src = join(root, f)
|
||||
target = src.replace(base, '/srv/pandora')
|
||||
rel_src = os.path.relpath(src, dirname(target))
|
||||
if os.path.exists(target):
|
||||
os.unlink(target)
|
||||
os.symlink(rel_src, target)
|
||||
if f in ('logo.%s.png'%name, 'icon.%s.png'%name):
|
||||
t = target.replace('%s.'%name, '')
|
||||
if os.path.exists(t):
|
||||
os.unlink(t)
|
||||
os.symlink(f, os.path.join(dirname(target), t))
|
||||
|
||||
'''
|
||||
overwrite = (
|
||||
('home', 'indiancinema'),
|
||||
)
|
||||
|
||||
os.chdir('/srv/pandora/static/js')
|
||||
for filename, sitename in overwrite:
|
||||
src = '%s.%s.js' % (filename, sitename)
|
||||
target = '%s.%s.js' % (filename, name)
|
||||
if os.path.exists(target):
|
||||
os.unlink(target)
|
||||
os.symlink(src, target)
|
||||
'''
|
||||
|
||||
os.chdir(base)
|
||||
src = join(base, 'config.jsonc')
|
||||
target = '/srv/pandora/pandora/config.%s.jsonc' % name
|
||||
rel_src = os.path.relpath(src, dirname(target))
|
||||
if os.path.exists(target):
|
||||
os.unlink(target)
|
||||
os.symlink(rel_src, target)
|
||||
t = '/srv/pandora/pandora/config.jsonc'
|
||||
if os.path.exists(t):
|
||||
os.unlink(t)
|
||||
os.symlink(basename(target), t)
|
||||
|
||||
for root, folders, files in os.walk(join(base, 'scripts')):
|
||||
for f in files:
|
||||
if f.endswith('.pyc'):
|
||||
continue
|
||||
src = join(root, f)
|
||||
target = src.replace(base, '/srv/pandora')
|
||||
rel_src = os.path.relpath(src, dirname(target))
|
||||
if os.path.exists(target):
|
||||
os.unlink(target)
|
||||
os.symlink(rel_src, target)
|
||||
if f == 'poster.%s.py' % name:
|
||||
t = os.path.join(dirname(target), 'poster.py')
|
||||
if os.path.exists(t):
|
||||
os.unlink(t)
|
||||
os.symlink(f, os.path.join(dirname(target), t))
|
||||
|
||||
local_settings_py = '/srv/pandora/pandora/local_settings.py'
|
||||
with open(local_settings_py) as fd:
|
||||
local_settings_changed = False
|
||||
local_settings = fd.read()
|
||||
if not 'LOCAL_APPS' in local_settings:
|
||||
local_settings += '\nLOCAL_APPS = ["%s"]\n' % name
|
||||
local_settings_changed = True
|
||||
else:
|
||||
apps = re.compile('(LOCAL_APPS.*?)\]', re.DOTALL).findall(local_settings)[0]
|
||||
if not name in apps:
|
||||
new_apps = apps.strip() + ',\n"%s"\n' % name
|
||||
local_settings = local_settings.replace(apps, new_apps)
|
||||
local_settings_changed = True
|
||||
if local_settings_changed:
|
||||
with open(local_settings_py, 'w') as fd:
|
||||
fd.write(local_settings)
|
||||
|
653
static/js/infoView.ubu.js
Normal file
653
static/js/infoView.ubu.js
Normal file
|
@ -0,0 +1,653 @@
|
|||
'use strict';
|
||||
|
||||
pandora.ui.infoView = function(data) {
|
||||
|
||||
var ui = pandora.user.ui,
|
||||
descriptions = [],
|
||||
canEdit = pandora.site.capabilities.canEditMetadata[pandora.user.level] || data.editable,
|
||||
canRemove = pandora.site.capabilities.canRemoveItems[pandora.user.level],
|
||||
css = {
|
||||
marginTop: '4px',
|
||||
textAlign: 'justify'
|
||||
},
|
||||
html,
|
||||
iconRatio = ui.icons == 'posters' ? data.posterRatio : 1,
|
||||
iconSize = ui.infoIconSize,
|
||||
iconWidth = iconRatio > 1 ? iconSize : Math.round(iconSize * iconRatio),
|
||||
iconHeight = iconRatio < 1 ? iconSize : Math.round(iconSize / iconRatio),
|
||||
iconLeft = iconSize == 256 ? Math.floor((iconSize - iconWidth) / 2) : 0,
|
||||
borderRadius = ui.icons == 'posters' ? 0 : iconSize / 8,
|
||||
margin = 16,
|
||||
nameKeys = ['director'],
|
||||
listKeys = nameKeys.concat(['country', 'groups']),
|
||||
statisticsWidth = 128,
|
||||
|
||||
$bar = Ox.Bar({size: 16})
|
||||
.bindEvent({
|
||||
doubleclick: function(e) {
|
||||
if ($(e.target).is('.OxBar')) {
|
||||
$info.animate({scrollTop: 0}, 250);
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
$options = Ox.MenuButton({
|
||||
items: [
|
||||
{
|
||||
id: 'delete',
|
||||
title: Ox._('Delete {0}...', [pandora.site.itemName.singular]),
|
||||
disabled: !canRemove
|
||||
}
|
||||
],
|
||||
style: 'square',
|
||||
title: 'set',
|
||||
tooltip: Ox._('Options'),
|
||||
type: 'image',
|
||||
})
|
||||
.css({
|
||||
float: 'left',
|
||||
borderColor: 'rgba(0, 0, 0, 0)',
|
||||
background: 'rgba(0, 0, 0, 0)'
|
||||
})
|
||||
.bindEvent({
|
||||
click: function(data_) {
|
||||
if (data_.id == 'delete') {
|
||||
pandora.$ui.deleteItemDialog = pandora.ui.deleteItemDialog(data).open();
|
||||
}
|
||||
}
|
||||
})
|
||||
.appendTo($bar),
|
||||
|
||||
$edit = Ox.MenuButton({
|
||||
items: [
|
||||
{
|
||||
id: 'insert',
|
||||
title: Ox._('Insert HTML...'),
|
||||
disabled: true
|
||||
}
|
||||
],
|
||||
style: 'square',
|
||||
title: 'edit',
|
||||
tooltip: Ox._('Edit'),
|
||||
type: 'image',
|
||||
})
|
||||
.css({
|
||||
float: 'right',
|
||||
borderColor: 'rgba(0, 0, 0, 0)',
|
||||
background: 'rgba(0, 0, 0, 0)'
|
||||
})
|
||||
.bindEvent({
|
||||
click: function(data) {
|
||||
// ...
|
||||
}
|
||||
})
|
||||
.appendTo($bar),
|
||||
|
||||
$info = Ox.Element().css({overflowY: 'auto'}),
|
||||
|
||||
that = Ox.SplitPanel({
|
||||
elements: [
|
||||
{element: $bar, size: 16},
|
||||
{element: $info}
|
||||
],
|
||||
orientation: 'vertical'
|
||||
}),
|
||||
|
||||
$icon = Ox.Element({
|
||||
element: '<img>',
|
||||
})
|
||||
.attr({
|
||||
src: '/' + data.id + '/' + (
|
||||
ui.icons == 'posters' ? 'poster' : 'icon'
|
||||
) + '512.jpg?' + data.modified
|
||||
})
|
||||
.css({
|
||||
position: 'absolute',
|
||||
left: margin + iconLeft + 'px',
|
||||
top: margin + 'px',
|
||||
width: iconWidth + 'px',
|
||||
height: iconHeight + 'px',
|
||||
borderRadius: borderRadius + 'px',
|
||||
cursor: 'pointer'
|
||||
})
|
||||
.bindEvent({
|
||||
singleclick: toggleIconSize
|
||||
})
|
||||
.appendTo($info),
|
||||
|
||||
$reflection = $('<div>')
|
||||
.addClass('OxReflection')
|
||||
.css({
|
||||
position: 'absolute',
|
||||
left: margin + 'px',
|
||||
top: margin + iconHeight + 'px',
|
||||
width: iconSize + 'px',
|
||||
height: iconSize / 2 + 'px',
|
||||
overflow: 'hidden'
|
||||
})
|
||||
.appendTo($info),
|
||||
|
||||
$reflectionIcon = $('<img>')
|
||||
.attr({
|
||||
src: '/' + data.id + '/' + (
|
||||
ui.icons == 'posters' ? 'poster' : 'icon'
|
||||
) + '512.jpg?' + data.modified
|
||||
})
|
||||
.css({
|
||||
position: 'absolute',
|
||||
left: iconLeft + 'px',
|
||||
width: iconWidth + 'px',
|
||||
height: iconHeight + 'px',
|
||||
borderRadius: borderRadius + 'px'
|
||||
})
|
||||
.appendTo($reflection),
|
||||
|
||||
$reflectionGradient = $('<div>')
|
||||
.css({
|
||||
position: 'absolute',
|
||||
width: iconSize + 'px',
|
||||
height: iconSize / 2 + 'px'
|
||||
})
|
||||
.appendTo($reflection),
|
||||
|
||||
$text = Ox.Element()
|
||||
.addClass('OxTextPage')
|
||||
.css({
|
||||
position: 'absolute',
|
||||
left: margin + (iconSize == 256 ? 256 : iconWidth) + margin + 'px',
|
||||
top: margin + 'px',
|
||||
right: margin + statisticsWidth + margin + 'px',
|
||||
})
|
||||
.appendTo($info),
|
||||
|
||||
$statistics = $('<div>')
|
||||
.css({
|
||||
position: 'absolute',
|
||||
width: statisticsWidth + 'px',
|
||||
top: margin + 'px',
|
||||
right: margin + 'px'
|
||||
})
|
||||
.appendTo($info),
|
||||
|
||||
$capabilities;
|
||||
|
||||
[$options, $edit].forEach(function($element) {
|
||||
$element.find('input').css({
|
||||
borderWidth: 0,
|
||||
borderRadius: 0,
|
||||
padding: '3px'
|
||||
});
|
||||
});
|
||||
|
||||
if (!canEdit) {
|
||||
pandora.createLinks($info);
|
||||
}
|
||||
|
||||
// Title -------------------------------------------------------------------
|
||||
|
||||
$('<div>')
|
||||
.css({
|
||||
marginTop: '-2px',
|
||||
})
|
||||
.append(
|
||||
Ox.EditableContent({
|
||||
editable: canEdit,
|
||||
tooltip: canEdit ? pandora.getEditTooltip() : '',
|
||||
value: data.title || ''
|
||||
})
|
||||
.css({
|
||||
marginBottom: '-3px',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '13px'
|
||||
})
|
||||
.bindEvent({
|
||||
submit: function(event) {
|
||||
editMetadata('title', event.value);
|
||||
}
|
||||
})
|
||||
)
|
||||
.appendTo($text);
|
||||
|
||||
// Director, Year and Country ----------------------------------------------
|
||||
|
||||
renderGroup(['director', 'year', 'country']);
|
||||
|
||||
renderGroup(['ubu']);
|
||||
|
||||
// Summary -----------------------------------------------------------------
|
||||
|
||||
if (canEdit || data.summary) {
|
||||
$('<div>')
|
||||
.append(
|
||||
Ox.EditableContent({
|
||||
clickLink: pandora.clickLink,
|
||||
editable: canEdit,
|
||||
format: function(value) {
|
||||
return value.replace(
|
||||
/<img src=/g,
|
||||
'<img style="float: left; max-width: 256px; max-height: 256px; margin: 0 16px 16px 0" src='
|
||||
);
|
||||
},
|
||||
maxHeight: Infinity,
|
||||
placeholder: formatLight(Ox._('No Summary')),
|
||||
tooltip: canEdit ? pandora.getEditTooltip() : '',
|
||||
type: 'textarea',
|
||||
value: data.summary || ''
|
||||
})
|
||||
.css(css)
|
||||
.css({
|
||||
marginTop: '12px',
|
||||
overflow: 'hidden'
|
||||
})
|
||||
.bindEvent({
|
||||
submit: function(event) {
|
||||
editMetadata('summary', event.value);
|
||||
}
|
||||
})
|
||||
)
|
||||
.appendTo($text);
|
||||
}
|
||||
|
||||
// Duration, Aspect Ratio --------------------------------------------------
|
||||
|
||||
['duration', 'aspectratio'].forEach(function(key) {
|
||||
var itemKey = Ox.getObjectById(pandora.site.itemKeys, key),
|
||||
value = data[key] || 0;
|
||||
$('<div>')
|
||||
.css({marginBottom: '4px'})
|
||||
.append(formatKey(itemKey.title, 'statistics'))
|
||||
.append(
|
||||
Ox.Theme.formatColor(null, 'gradient')
|
||||
.css({textAlign: 'right'})
|
||||
.html(
|
||||
Ox['format' + Ox.toTitleCase(itemKey.format.type)]
|
||||
.apply(null, [value].concat(itemKey.format.args))
|
||||
)
|
||||
)
|
||||
.appendTo($statistics);
|
||||
});
|
||||
|
||||
// Hue, Saturation, Lightness, Volume --------------------------------------
|
||||
|
||||
['hue', 'saturation', 'lightness', 'volume'].forEach(function(key) {
|
||||
$('<div>')
|
||||
.css({marginBottom: '4px'})
|
||||
.append(formatKey(key, 'statistics'))
|
||||
.append(
|
||||
Ox.Theme.formatColor(
|
||||
data[key] || 0, key == 'volume' ? 'lightness' : key
|
||||
).css({textAlign: 'right'})
|
||||
)
|
||||
.appendTo($statistics);
|
||||
});
|
||||
|
||||
// Cuts per Minute ---------------------------------------------------------
|
||||
|
||||
$('<div>')
|
||||
.css({marginBottom: '4px'})
|
||||
.append(formatKey('cuts per minute', 'statistics'))
|
||||
.append(
|
||||
Ox.Theme.formatColor(null, 'gradient')
|
||||
.css({textAlign: 'right'})
|
||||
.html(Ox.formatNumber(data['cutsperminute'] || 0, 3))
|
||||
)
|
||||
.appendTo($statistics);
|
||||
|
||||
// Rights Level ------------------------------------------------------------
|
||||
|
||||
var $rightsLevel = $('<div>');
|
||||
$('<div>')
|
||||
.css({marginBottom: '4px'})
|
||||
.append(formatKey('Rights Level', 'statistics'))
|
||||
.append($rightsLevel)
|
||||
.appendTo($statistics);
|
||||
renderRightsLevel();
|
||||
|
||||
// Notes --------------------------------------------------------------------
|
||||
|
||||
if (canEdit) {
|
||||
$('<div>')
|
||||
.css({marginBottom: '4px'})
|
||||
.append(
|
||||
formatKey('Notes', 'statistics').options({
|
||||
tooltip: Ox._('Only {0} can see and edit these comments', [
|
||||
Object.keys(pandora.site.capabilities.canEditMetadata).map(function(level, i) {
|
||||
return (
|
||||
i == 0 ? ''
|
||||
: i < Ox.len(pandora.site.capabilities.canEditMetadata) - 1 ? ', '
|
||||
: ' ' + Ox._('and') + ' '
|
||||
) + Ox.toTitleCase(level)
|
||||
}).join('')])
|
||||
})
|
||||
)
|
||||
.append(
|
||||
Ox.EditableContent({
|
||||
height: 128,
|
||||
placeholder: formatLight(Ox._('No notes')),
|
||||
tooltip: pandora.getEditTooltip(),
|
||||
type: 'textarea',
|
||||
value: data.notes || '',
|
||||
width: 128
|
||||
})
|
||||
.bindEvent({
|
||||
submit: function(event) {
|
||||
editMetadata('notes', event.value);
|
||||
}
|
||||
})
|
||||
)
|
||||
.appendTo($statistics);
|
||||
}
|
||||
|
||||
$('<div>').css({height: '16px'}).appendTo($statistics);
|
||||
|
||||
function editMetadata(key, value) {
|
||||
if (value != data[key]) {
|
||||
var edit = {id: data.id};
|
||||
if (key == 'title') {
|
||||
edit[key] = value;
|
||||
} else if (listKeys.indexOf(key) >= 0) {
|
||||
edit[key] = value ? value.split(', ') : [];
|
||||
} else {
|
||||
edit[key] = value ? value : null;
|
||||
}
|
||||
pandora.api.edit(edit, function(result) {
|
||||
var src;
|
||||
data[key] = result.data[key];
|
||||
descriptions[key] && descriptions[key].options({
|
||||
value: result.data[key + 'description']
|
||||
});
|
||||
Ox.Request.clearCache(); // fixme: too much? can change filter/list etc
|
||||
if (result.data.id != data.id) {
|
||||
pandora.UI.set({item: result.data.id});
|
||||
pandora.$ui.browser.value(data.id, 'id', result.data.id);
|
||||
}
|
||||
pandora.updateItemContext();
|
||||
pandora.$ui.browser.value(result.data.id, key, result.data[key]);
|
||||
pandora.$ui.itemTitle
|
||||
.options({
|
||||
title: '<b>' + result.data.title
|
||||
+ (Ox.len(result.data.director)
|
||||
? ' (' + result.data.director.join(', ') + ')'
|
||||
: '')
|
||||
+ (result.data.year ? ' ' + result.data.year : '') + '</b>'
|
||||
});
|
||||
if (
|
||||
Ox.contains(['title', 'director', 'year'], key)
|
||||
&& ui.icons == 'posters'
|
||||
) {
|
||||
src = '/' + data.id + '/poster512.jpg?' + Ox.uid()
|
||||
$icon.attr({src: src});
|
||||
$reflectionIcon.attr({src: src});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function formatKey(key, mode) {
|
||||
var item = Ox.getObjectById(pandora.site.itemKeys, key);
|
||||
key = Ox._(item ? item.title : key);
|
||||
mode = mode || 'text';
|
||||
return mode == 'text'
|
||||
? '<span style="font-weight: bold">' + Ox.toTitleCase(key) + ':</span> '
|
||||
: mode == 'description'
|
||||
? Ox.toTitleCase(key)
|
||||
: Ox.Element()
|
||||
.css({marginBottom: '4px', fontWeight: 'bold'})
|
||||
.html(Ox.toTitleCase(key)
|
||||
.replace(' Per ', ' per '));
|
||||
}
|
||||
|
||||
function formatLight(str) {
|
||||
return '<span class="OxLight">' + str + '</span>';
|
||||
}
|
||||
|
||||
function formatLink(value, key) {
|
||||
return (Ox.isArray(value) ? value : [value]).map(function(value) {
|
||||
return key
|
||||
? '<a href="/' + key + '=' + value + '">' + value + '</a>'
|
||||
: value;
|
||||
}).join(', ');
|
||||
}
|
||||
|
||||
function formatValue(key, value) {
|
||||
var ret;
|
||||
if (key == 'year') {
|
||||
ret = formatLink(value, 'year');
|
||||
} else if (nameKeys.indexOf(key) > -1) {
|
||||
ret = formatLink(value.split(', '), 'name');
|
||||
} else if (listKeys.indexOf(key) > -1) {
|
||||
ret = formatLink(value.split(', '), key);
|
||||
} else if (key == 'ubu') {
|
||||
ret = '<a href="http://ubu.com/' + value + '.html">Open on UBU</a>';
|
||||
} else {
|
||||
ret = value;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
function getRightsLevelElement(rightsLevel) {
|
||||
return Ox.Theme.formatColorLevel(
|
||||
rightsLevel,
|
||||
pandora.site.rightsLevels.map(function(rightsLevel) {
|
||||
return rightsLevel.name;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function getValue(key, value) {
|
||||
return !value ? ''
|
||||
: Ox.contains(listKeys, key) ? value.join(', ')
|
||||
: value;
|
||||
}
|
||||
|
||||
function renderCapabilities(rightsLevel) {
|
||||
var capabilities = [].concat(
|
||||
canEdit ? [{name: 'canSeeItem', symbol: 'Find'}] : [],
|
||||
[
|
||||
{name: 'canPlayClips', symbol: 'PlayInToOut'},
|
||||
{name: 'canPlayVideo', symbol: 'Play'},
|
||||
{name: 'canDownloadVideo', symbol: 'Download'}
|
||||
]
|
||||
),
|
||||
userLevels = canEdit ? pandora.site.userLevels : [pandora.user.level];
|
||||
$capabilities.empty();
|
||||
userLevels.forEach(function(userLevel, i) {
|
||||
var $element,
|
||||
$line = $('<div>')
|
||||
.css({
|
||||
height: '16px',
|
||||
marginBottom: '4px'
|
||||
})
|
||||
.appendTo($capabilities);
|
||||
if (canEdit) {
|
||||
$element = Ox.Theme.formatColorLevel(i, userLevels.map(function(userLevel) {
|
||||
return Ox.toTitleCase(userLevel);
|
||||
}), [0, 240]);
|
||||
Ox.Label({
|
||||
textAlign: 'center',
|
||||
title: Ox.toTitleCase(userLevel),
|
||||
width: 60
|
||||
})
|
||||
.addClass('OxColor OxColorGradient')
|
||||
.css({
|
||||
float: 'left',
|
||||
height: '12px',
|
||||
paddingTop: '2px',
|
||||
background: $element.css('background'),
|
||||
fontSize: '8px',
|
||||
color: $element.css('color')
|
||||
})
|
||||
.data({OxColor: $element.data('OxColor')})
|
||||
.appendTo($line);
|
||||
}
|
||||
capabilities.forEach(function(capability) {
|
||||
var hasCapability = pandora.site.capabilities[capability.name][userLevel] >= rightsLevel,
|
||||
$element = Ox.Theme.formatColorLevel(hasCapability, ['', '']);
|
||||
Ox.Button({
|
||||
tooltip: (canEdit ? Ox.toTitleCase(userLevel) : 'You') + ' '
|
||||
+ (hasCapability ? 'can' : 'can\'t') + ' '
|
||||
+ Ox.toSlashes(capability.name)
|
||||
.split('/').slice(1).join(' ')
|
||||
.toLowerCase(),
|
||||
title: capability.symbol,
|
||||
type: 'image'
|
||||
})
|
||||
.addClass('OxColor OxColorGradient')
|
||||
.css({background: $element.css('background')})
|
||||
.css('margin' + (canEdit ? 'Left' : 'Right'), '4px')
|
||||
.data({OxColor: $element.data('OxColor')})
|
||||
.appendTo($line);
|
||||
});
|
||||
if (!canEdit) {
|
||||
Ox.Button({
|
||||
title: Ox._('Help'),
|
||||
tooltip: Ox._('About Rights'),
|
||||
type: 'image'
|
||||
})
|
||||
.css({marginLeft: '52px'})
|
||||
.bindEvent({
|
||||
click: function() {
|
||||
pandora.UI.set({page: 'rights'});
|
||||
}
|
||||
})
|
||||
.appendTo($line);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderGroup(keys) {
|
||||
var $element;
|
||||
if (canEdit || keys.filter(function(key) {
|
||||
return data[key];
|
||||
}).length) {
|
||||
$element = $('<div>').addClass('OxSelectable').css(css);
|
||||
keys.forEach(function(key, i) {
|
||||
if (canEdit || data[key]) {
|
||||
if ($element.children().length) {
|
||||
$('<span>').html('; ').appendTo($element);
|
||||
}
|
||||
$('<span>').html(formatKey(key)).appendTo($element);
|
||||
Ox.EditableContent({
|
||||
clickLink: pandora.clickLink,
|
||||
format: function(value) {
|
||||
return formatValue(key, value);
|
||||
},
|
||||
placeholder: formatLight(Ox._('unknown')),
|
||||
tooltip: canEdit ? pandora.getEditTooltip() : '',
|
||||
value: getValue(key, data[key])
|
||||
})
|
||||
.bindEvent({
|
||||
submit: function(data) {
|
||||
editMetadata(key, data.value);
|
||||
}
|
||||
})
|
||||
.appendTo($element);
|
||||
}
|
||||
});
|
||||
$element.appendTo($text);
|
||||
}
|
||||
}
|
||||
|
||||
function renderRightsLevel() {
|
||||
var $rightsLevelElement = getRightsLevelElement(data.rightslevel),
|
||||
$rightsLevelSelect;
|
||||
$rightsLevel.empty();
|
||||
if (canEdit) {
|
||||
$rightsLevelSelect = Ox.Select({
|
||||
items: pandora.site.rightsLevels.map(function(rightsLevel, i) {
|
||||
return {id: i, title: rightsLevel.name};
|
||||
}),
|
||||
width: 128,
|
||||
value: data.rightslevel
|
||||
})
|
||||
.addClass('OxColor OxColorGradient')
|
||||
.css({
|
||||
marginBottom: '4px',
|
||||
background: $rightsLevelElement.css('background')
|
||||
})
|
||||
.data({OxColor: $rightsLevelElement.data('OxColor')})
|
||||
.bindEvent({
|
||||
change: function(event) {
|
||||
var rightsLevel = event.value;
|
||||
$rightsLevelElement = getRightsLevelElement(rightsLevel);
|
||||
$rightsLevelSelect
|
||||
.css({background: $rightsLevelElement.css('background')})
|
||||
.data({OxColor: $rightsLevelElement.data('OxColor')})
|
||||
renderCapabilities(rightsLevel);
|
||||
pandora.api.edit({id: data.id, rightslevel: rightsLevel}, function(result) {
|
||||
// ...
|
||||
});
|
||||
}
|
||||
})
|
||||
.appendTo($rightsLevel);
|
||||
} else {
|
||||
$rightsLevelElement
|
||||
.css({
|
||||
marginBottom: '4px'
|
||||
})
|
||||
.appendTo($rightsLevel);
|
||||
}
|
||||
$capabilities = $('<div>').appendTo($rightsLevel);
|
||||
renderCapabilities(data.rightslevel);
|
||||
}
|
||||
|
||||
function toggleIconSize() {
|
||||
iconSize = iconSize == 256 ? 512 : 256;
|
||||
iconWidth = iconRatio > 1 ? iconSize : Math.round(iconSize * iconRatio);
|
||||
iconHeight = iconRatio < 1 ? iconSize : Math.round(iconSize / iconRatio);
|
||||
iconLeft = iconSize == 256 ? Math.floor((iconSize - iconWidth) / 2) : 0,
|
||||
borderRadius = ui.icons == 'posters' ? 0 : iconSize / 8;
|
||||
$icon.animate({
|
||||
left: margin + iconLeft + 'px',
|
||||
width: iconWidth + 'px',
|
||||
height: iconHeight + 'px',
|
||||
borderRadius: borderRadius + 'px'
|
||||
}, 250);
|
||||
$reflection.animate({
|
||||
top: margin + iconHeight + 'px',
|
||||
width: iconSize + 'px',
|
||||
height: iconSize / 2 + 'px'
|
||||
}, 250);
|
||||
$reflectionIcon.animate({
|
||||
left: iconLeft + 'px',
|
||||
width: iconWidth + 'px',
|
||||
height: iconHeight + 'px',
|
||||
borderRadius: borderRadius + 'px'
|
||||
}, 250);
|
||||
$reflectionGradient.animate({
|
||||
width: iconSize + 'px',
|
||||
height: iconSize / 2 + 'px'
|
||||
}, 250);
|
||||
$text.animate({
|
||||
left: margin + (iconSize == 256 ? 256 : iconWidth) + margin + 'px',
|
||||
}, 250);
|
||||
pandora.UI.set({infoIconSize: iconSize});
|
||||
}
|
||||
|
||||
that.reload = function() {
|
||||
var src = src = '/' + data.id + '/' + (
|
||||
ui.icons == 'posters' ? 'poster' : 'icon'
|
||||
) + '512.jpg?' + Ox.uid();
|
||||
$icon.attr({src: src});
|
||||
$reflectionIcon.attr({src: src});
|
||||
iconSize = iconSize == 256 ? 512 : 256;
|
||||
iconRatio = ui.icons == 'posters'
|
||||
? (ui.showSitePosters ? pandora.site.posters.ratio : data.posterRatio) : 1;
|
||||
toggleIconSize();
|
||||
};
|
||||
|
||||
that.bindEvent({
|
||||
mousedown: function() {
|
||||
setTimeout(function() {
|
||||
!Ox.Focus.focusedElementIsInput() && that.gainFocus();
|
||||
});
|
||||
},
|
||||
pandora_icons: that.reload,
|
||||
pandora_showsiteposters: function() {
|
||||
ui.icons == 'posters' && that.reload();
|
||||
}
|
||||
});
|
||||
|
||||
return that;
|
||||
|
||||
};
|
Loading…
Reference in a new issue