diff --git a/pandora/config.indiancinema.jsonc b/pandora/config.indiancinema.jsonc
new file mode 100644
index 00000000..41ef4ad9
--- /dev/null
+++ b/pandora/config.indiancinema.jsonc
@@ -0,0 +1,797 @@
+/*
+ indiancine.ma Settings
+
+ You can edit this file.
+*/
+{
+ "additionalSort": [
+ {"key": "director", "operator": "+"},
+ {"key": "year", "operator": "-"},
+ {"key": "title", "operator": "+"}
+ ],
+ "annotations": {
+ "showUsers": false
+ },
+ "cantPlay": {
+ "icon": "NoCopyright",
+ "link": "/rights",
+ "text": "This film is not out of copyright yet, come back in 60 years"
+ },
+ /*
+ Capabilities are per user level.
+ They can either be general:
+ {level: true} means a user of that level has the capability)
+ or related to items:
+ {level: x} means a user of that level has the capability
+ for items of a rights level up to and including x
+ */
+ "capabilities": {
+ "canDeleteItems": {"admin": true},
+ "canDownloadVideo": {"guest": -1, "member": -1, "student": -1, "staff": -1, "admin": -1},
+ "canEditAnnotations": {"staff": true, "admin": true},
+ "canEditEvents": {"staff": true, "admin": true},
+ "canEditFeaturedLists": {"staff": true, "admin": true},
+ "canEditFeaturedTexts": {"staff": true, "admin": true},
+ "canEditFeaturedEdits": {"staff": true, "admin": true},
+ "canEditMetadata": {"staff": true, "admin": true},
+ "canEditPlaces": {"staff": true, "admin": true},
+ "canEditSitePages": {"staff": true, "admin": true},
+ "canEditUsers": {"admin": true},
+ "canImportAnnotations": {},
+ "canManageTitlesAndNames": {"staff": true, "admin": true},
+ "canManagePlacesAndEvents": {"staff": true, "admin": true},
+ "canManageUsers": {"staff": true, "admin": true},
+ "canPlayClips": {"guest": 2, "member": 2, "student": 4, "staff": 4, "admin": 4},
+ "canPlayVideo": {"guest": 1, "member": 1, "student": 4, "staff": 4, "admin": 4},
+ "canSeeAccessed": {"staff": true, "admin": true},
+ "canSeeDebugMenu": {"staff": true, "admin": true},
+ "canSeeExtraItemViews": {"staff": true, "admin": true},
+ "canSeeFiles": {"staff": true, "admin": true},
+ "canSeeItem": {"guest": 3, "member": 3, "student": 4, "staff": 4, "admin": 4},
+ "canSeeSize": {"student": true, "staff": true, "admin": true},
+ "canSeeSoftwareVersion": {"staff": true, "admin": true},
+ "canSendMail": {"staff": true, "admin": true},
+ "canUploadVideo": {"guest": false, "member": false, "staff": true, "admin": true}
+ },
+ /*
+ clipKeys are the properties that clips can by sorted by.
+ If sortOperator is not specified, it will be + for strings and - 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. Excluding a layer from this list means it will not be
+ included in find annotations.
+ */
+ "clipLayers": ["subtitles"],
+ // fixme: either this, or filter: true in itemKeys, but not both
+ "filters": [
+ {"id": "director", "title": "Director", "type": "string"},
+ {"id": "country", "title": "Country", "type": "string"},
+ {"id": "year", "title": "Year", "type": "integer"},
+ {"id": "language", "title": "Language", "type": "string"},
+ {"id": "genre", "title": "Genre", "type": "string"},
+ {"id": "writer", "title": "Writer", "type": "string"},
+ {"id": "producer", "title": "Producer", "type": "string"},
+ {"id": "cinematographer", "title": "Cinematographer", "type": "string"},
+ {"id": "editor", "title": "Editor", "type": "string"},
+ {"id": "actor", "title": "Actor", "type": "string"},
+ {"id": "keyword", "title": "Keyword", "type": "string"}
+ ],
+ /*
+ An itemKey must have the following properties:
+ id: The id of the key (as known by the server)
+ title: The title of the key (as displayed by the client)
+ type: text, string, float, integer, or array of any of these
+ and can have any of the following properties:
+ autocomplete: If true, find element will autocomplete
+ autocompleteSortKey: The key that suggestions will be sorted by
+ capability: A capability required to see this key
+ columnRequired: If true, the column can't be removed
+ columnWidth: Default column width in px
+ filter: If true, one can filter results by this key
+ find: If true, this key will appear as a find option
+ format: {type: "...", args: [...]}, for special formatting
+ (Ox.formatType(args) will be called)
+ sort: If true, one can sort results by this key
+ sortOperator: sort operator (+, -), in case it differs from the
+ default for the key's type (+ for strings, - for numbers)
+ sortType: special sort type (title, person)
+ value: {key: "...", type: "..."}, for keys that are derived
+ from other keys (like number of actors), or "capability"
+ */
+ "itemKeys": [
+ {
+ "id": "*",
+ "title": "All",
+ "type": "text",
+ "find": true
+ },
+ {
+ "id": "title",
+ "title": "Title",
+ "type": "string",
+ "additionalSort": [{"key": "year", "operator": "-"}, {"key": "director", "operator": "+"}],
+ "autocomplete": true,
+ "autocompleteSortKey": "votes",
+ "columnRequired": true,
+ "columnWidth": 180,
+ "find": true,
+ "sort": true,
+ "sortType": "title"
+ },
+ {
+ "id": "director",
+ "title": "Director",
+ "type": ["string"],
+ "additionalSort": [{"key": "year", "operator": "-"}, {"key": "title", "operator": "-"}],
+ "autocomplete": true,
+ "columnRequired": true,
+ "columnWidth": 180,
+ "filter": true,
+ "find": true,
+ "sort": true,
+ "sortType": "person"
+ },
+ {
+ "id": "country",
+ "title": "Country",
+ "type": ["string"],
+ "autocomplete": true,
+ "columnWidth": 120,
+ "filter": true,
+ "find": true,
+ "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": "language",
+ "title": "Language",
+ "type": ["string"],
+ "autocomplete": true,
+ "columnWidth": 120,
+ "filter": true,
+ "find": true,
+ "sort": true
+ },
+ {
+ "id": "runtime",
+ "title": "Runtime",
+ "type": "time",
+ "columnWidth": 60,
+ "format": {"type": "duration", "args": [0, "short"]},
+ "sort": true
+ },
+ {
+ "id": "writer",
+ "title": "Writer",
+ "type": ["string"],
+ "autocomplete": true,
+ "columnWidth": 180,
+ "filter": true,
+ "find": true,
+ "sort": true,
+ "sortType": "person"
+ },
+ {
+ "id": "producer",
+ "title": "Producer",
+ "type": ["string"],
+ "autocomplete": true,
+ "columnWidth": 180,
+ "filter": true,
+ "find": true,
+ "sort": true,
+ "sortType": "person"
+ },
+ {
+ "id": "cinematographer",
+ "title": "Cinematographer",
+ "type": ["string"],
+ "autocomplete": true,
+ "columnWidth": 180,
+ "filter": true,
+ "find": true,
+ "sort": true,
+ "sortType": "person"
+ },
+ {
+ "id": "editor",
+ "title": "Editor",
+ "type": ["string"],
+ "autocomplete": true,
+ "columnWidth": 180,
+ "filter": true,
+ "find": true,
+ "sort": true,
+ "sortType": "person"
+ },
+ {
+ "id": "actor",
+ "title": "Actor",
+ "type": ["string"],
+ "autocomplete": true,
+ "filter": true,
+ "find": true,
+ "sortType": "person"
+ },
+ {
+ "id": "numberofactors",
+ "title": "Number of Actors",
+ "type": "integer",
+ "columnWidth": 60,
+ "sort": true,
+ "value": {"key": "actor", "type": "length"}
+ },
+ {
+ "id": "character",
+ "title": "Character",
+ "type": ["string"],
+ "autocomplete": true,
+ "find": true,
+ "sortType": "string"
+ },
+ {
+ "id": "name",
+ "title": "Name",
+ "type": ["string"],
+ "autocomplete": true,
+ "find": true
+ },
+ {
+ "id": "series",
+ "title": "TV Series",
+ "type": "boolean"
+ },
+ {
+ "id": "genre",
+ "title": "Genre",
+ "type": ["string"],
+ "autocomplete": true,
+ "columnWidth": 120,
+ "filter": true,
+ "find": true,
+ "sort": true
+ },
+ {
+ "id": "summary",
+ "title": "Summary",
+ "type": "text",
+ "find": true
+ },
+ {
+ "id": "trivia",
+ "title": "Trivia",
+ "type": ["text"]
+ },
+ {
+ "id": "releasedate",
+ "title": "Release Date",
+ "type": "date",
+ "columnWidth": 120,
+ "format": {"type": "date", "args": ["%a, %b %e, %Y"]},
+ "sort": true
+ },
+ {
+ "id": "budget",
+ "title": "Budget",
+ "type": "integer",
+ "columnWidth": 90,
+ "format": {"type": "unit", "args": ["$"]},
+ "sort": true
+ },
+ {
+ "id": "gross",
+ "title": "Gross",
+ "type": "integer",
+ "columnWidth": 90,
+ "format": {"type": "unit", "args": ["$"]},
+ "sort": true
+ },
+ {
+ "id": "profit",
+ "title": "Profit",
+ "type": "integer",
+ "columnWidth": 90,
+ "format": {"type": "unit", "args": ["$"]},
+ "sort": true
+ },
+ {
+ "id": "votes",
+ "title": "Mainstream Score",
+ "type": "float",
+ "columnWidth": 60,
+ "format": {"type": "ColorPercent", "args": [1, true]},
+ "sort": true
+ },
+ {
+ "id": "likes",
+ "title": "Arthouse Score",
+ "type": "float",
+ "columnWidth": 60,
+ "format": {"type": "ColorPercent", "args": [1, true]},
+ "sort": true
+ },
+ {
+ "id": "id",
+ "title": "ID",
+ "type": "string",
+ "columnWidth": 90,
+ "sort": true
+ },
+ {
+ "id": "subtitles",
+ "title": "Subtitles",
+ "type": "layer",
+ "find": true
+ },
+ {
+ "id": "duration",
+ "title": "Duration",
+ "type": "float",
+ "columnWidth": 90,
+ "format": {"type": "duration", "args": []},
+ "sort": true
+ },
+ {
+ "id": "resolution",
+ "title": "Resolution",
+ "type": ["integer"],
+ "capability": "canSeeFiles",
+ "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",
+ "capability": "canSeeFiles",
+ "columnWidth": 90,
+ "format": {"type": "value", "args": ["px"]},
+ "sort": true
+ },
+ {
+ "id": "hue",
+ "title": "Hue",
+ "type": "float",
+ "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": "canSeeSize",
+ "columnWidth": 60,
+ "format": {"type": "value", "args": ["B"]},
+ "sort": true
+ },
+ {
+ "id": "bitrate",
+ "title": "Bitrate",
+ "type": "integer",
+ "capability": "canSeeFiles",
+ "columnWidth": 60,
+ "format": {"type": "value", "args": ["bps"]},
+ "sort": true
+ },
+ {
+ "id": "parts",
+ "title": "Number of Parts",
+ "type": "integer",
+ "capability": "canSeeFiles",
+ "columnWidth": 60,
+ "sort": true
+ },
+ {
+ "id": "numberoffiles",
+ "title": "Number of Files",
+ "type": "integer",
+ "capability": "canSeeFiles",
+ "columnWidth": 60,
+ "sort": true,
+ "value": {"key": "files", "type": "length"}
+ },
+ {
+ "id": "filename",
+ "title": "Filename",
+ "type": ["string"],
+ "capability": "canSeeFiles",
+ "find": true
+ },
+ {
+ "id": "created",
+ "title": "Date Created",
+ "type": "date",
+ "columnWidth": 150,
+ "format": {"type": "date", "args": ["%Y-%m-%d %H:%M:%S"]},
+ "sort": true
+ },
+ {
+ "id": "modified",
+ "title": "Last Modified",
+ "type": "date",
+ "columnWidth": 150,
+ "format": {"type": "date", "args": ["%Y-%m-%d %H:%M:%S"]},
+ "sort": true
+ },
+ {
+ "id": "accessed",
+ "title": "Last Accessed",
+ "type": "date",
+ "capability": "canSeeAccessed",
+ "columnWidth": 150,
+ "format": {"type": "date", "args": ["%Y-%m-%d %H:%M:%S"]},
+ "sort": true
+ },
+ {
+ "id": "timesaccessed",
+ "title": "Times Accessed",
+ "type": "integer",
+ "capability": "canSeeAccessed",
+ "columnWidth": 60,
+ "sort": true
+ },
+ {
+ "id": "rightslevel",
+ "title": "Rights Level",
+ "type": "enum",
+ "columnWidth": 90,
+ "format": {"type": "ColorLevel", "args": [
+ ["Public", "Relaxed", "Regular", "Restricted", "Private"]
+ ]},
+ "sort": true,
+ "sortOperator": "+",
+ "values": ["Public", "Relaxed", "Regular", "Restricted", "Private", "Unknown"]
+ },
+ {
+ "id": "canplayvideo",
+ "title": "Can Play Video",
+ "type": "boolean",
+ "value": "capability"
+ },
+ {
+ "id": "canplayclips",
+ "title": "Can Play Clips",
+ "type": "boolean",
+ "value": "capability"
+ },
+ {
+ "id": "random",
+ "title": "Random",
+ "type": "integer",
+ "sort": true
+ }
+ ],
+ /*
+ itemName specifies how items are being referred to.
+ Anything excessively long may cause layout errors.
+ */
+ "itemName": {
+ "singular": "Movie",
+ "plural": "Movies"
+ },
+ "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": "data", "title": "Data"},
+ {"id": "files", "title": "Files"}
+ ],
+ // fixme: should be renamed to annotationLayers
+ "layers": [
+ {
+ "id": "publickeywords",
+ "title": "Public Keywords",
+ "canAddAnnotations": {"member": true, "student": true, "staff": true, "admin": true},
+ "item": "Public Keyword",
+ "overlap": true,
+ "type": "string"
+ },
+ {
+ "id": "pirvatekeywords",
+ "title": "Private Keywords",
+ "canAddAnnotations": {"member": true, "student": true, "staff": true, "admin": true},
+ "item": "Private Keyword",
+ "overlap": true,
+ "type": "string"
+ },
+ {
+ "id": "publicnotes",
+ "title": "Public Notes",
+ "canAddAnnotations": {"member": true, "student": true, "staff": true, "admin": true},
+ "item": "Public Note",
+ "overlap": true,
+ "showInfo": true,
+ "type": "text"
+ },
+ {
+ "id": "privatenotes",
+ "title": "Private Notes",
+ "canAddAnnotations": {"member": true, "student": true, "staff": true, "admin": true},
+ "item": "Private Note",
+ "overlap": true,
+ "private": true,
+ "showInfo": true,
+ "type": "text"
+ },
+ {
+ "id": "publicnotes",
+ "title": "Public Notes",
+ "canAddAnnotations": {"member": true, "student": 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"
+ }
+ ],
+ "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": {
+ "importPosters": true,
+ "importFrames": true
+ },
+ "personalLists": [
+ {"title": "Favorites"},
+ {"title": "Silent Films", "query": {"conditions": [{"key": "language", "value": "None", "operator": "=="}], "operator": "&"}}
+ ],
+ "rightsLevel": {"member": 4, "staff": 3, "admin": 2},
+ "rightsLevels": [
+ {"name": "Public", "color": [128, 255, 128]},
+ {"name": "Relaxed", "color": [192, 255, 128]},
+ {"name": "Regular", "color": [255, 255, 128]},
+ {"name": "Restricted", "color": [255, 192, 128]},
+ {"name": "Private", "color": [255, 128, 128]}
+ ],
+ "sendReferrer": false,
+ "site": {
+ "description": "indiancine.ma proposes is an entirely new approach to visualizing and navigating moving images, and we hope that it can serve as a point of reference for individuals and institutions who are dealing with large collections of films.",
+ // FIXME: "from" and "to" would be more intuitive as keys here
+ "email": {
+ // E-mail address in contact form (to)
+ "contact": "contact@indiancine.ma",
+ "footer": "-- \nIndian Cinema - https://indiancine.ma",
+ "prefix": "indiancine.ma newsletter -",
+ // E-mail address uses by the system (from)
+ "system": "system@indiancine.ma"
+ },
+ "id": "indiancinema",
+ "name": "indiancine.ma",
+ "url": "indiancine.ma",
+ "videoprefix": ""
+ },
+ "sitePages": [
+ {"id": "about", "title": "About"},
+ {"id": "news", "title": "News"},
+ {"id": "faq", "title": "Frequently Asked Questions"},
+ {"id": "terms", "title": "Terms of Service"},
+ {"id": "contact", "title": "Contact"}
+ ],
+ "themes": ["oxlight", "oxmedium", "oxdark"],
+ "timelines": [
+ {"id": "antialias", "title": "Anti-Alias"},
+ {"id": "slitscan", "title": "Slit-Scan"},
+ {"id": "keyframes", "title": "Keyframes"},
+ {"id": "audio", "title": "Waveform"}
+ ],
+ "totals": [
+ {"id": "items"},
+ {"id": "runtime"},
+ {"id": "files", "admin": true},
+ {"id": "duration", "admin": true},
+ {"id": "size", "admin": true},
+ {"id": "pixels"}
+ ],
+ "tv": {
+ "showLogo": false
+ },
+ "user": {
+ "level": "guest",
+ "newsletter": true,
+ "ui": {
+ "annotationsFont": "small",
+ "annotationsRange": "all",
+ "annotationsSize": 256,
+ "annotationsSort": "position",
+ "clipsColumns": 2,
+ "columns": {
+ "Colors": {
+ "columns": ["title", "director", "country", "year", "hue", "saturation", "brightness"],
+ "columnWidth": {}
+ }
+ },
+ "filters": [
+ {"id": "director", "sort": [{"key": "items", "operator": "-"}]},
+ {"id": "producer", "sort": [{"key": "items", "operator": "-"}]},
+ {"id": "year", "sort": [{"key": "name", "operator": "-"}]},
+ {"id": "language", "sort": [{"key": "items", "operator": "-"}]},
+ {"id": "genre", "sort": [{"key": "items", "operator": "-"}]}
+ ],
+ "filtersSize": 176,
+ "find": {"conditions": [], "operator": "&"},
+ "followPlayer": true,
+ "icons": "posters",
+ "infoIconSize": 256,
+ "item": "",
+ "text": "",
+ "edit": "",
+ "itemFind": "",
+ "itemSort": [{"key": "position", "operator": "+"}],
+ "itemView": "info",
+ "listColumns": ["title", "director", "country", "year", "language", "runtime", "genre"],
+ "listColumnWidth": {},
+ "listSelection": [],
+ "listSort": [{"key": "director", "operator": "+"}],
+ "listView": "grid",
+ "lists": {},
+ "mapFind": "",
+ "mapSelection": "",
+ "page": "",
+ "section": "items",
+ "sequenceMode": "shape",
+ "sequenceSort": [{"key": "director", "operator": "+"}],
+ "showAnnotations": true,
+ "showBrowser": true,
+ "showCalendarControls": true, // fixme: should be false
+ "showFilters": true,
+ "showFlags": true, // fixme: this should be a site preference
+ "showHome": true,
+ "showIconBrowser": false,
+ "showInfo": true,
+ "showLayers": {
+ "privatenotes": true,
+ "publicnotes": true,
+ "subtitles": true
+ },
+ "showMapControls": false,
+ "showMapLabels": false,
+ "showFolder": {
+ "items": {
+ "personal": true,
+ "favorite": true,
+ "featured": true,
+ "volumes": true
+ },
+ "texts": {
+ "personal": true,
+ "favorite": true,
+ "featured": true
+ }
+ },
+ "showSidebar": true,
+ "showSitePosters": false,
+ "showTimeline": true,
+ "sidebarSize": 256,
+ "theme": "oxlight",
+ "videoMuted": false,
+ "videoPoints": {},
+ "videoResolution": 96,
+ "videoScale": "fit",
+ "videoSize": "small",
+ "videoSubtitles": true,
+ "videoTimeline": "antialias",
+ "videoView": "player",
+ "videoVolume": 1
+ },
+ "username": "",
+ "volumes": []
+ },
+ // fixme: this should include colors
+ "userLevels": ["guest", "member", "student", "staff", "admin"],
+ "video": {
+ "download": false,
+ "formats": ["webm", "mp4"],
+ "previewRatio": 1.7777777778,
+ "resolutions": [480, 240]
+ }
+}
diff --git a/static/js/pandora/infoView.indiancinema.js b/static/js/pandora/infoView.indiancinema.js
new file mode 100644
index 00000000..90fa7e82
--- /dev/null
+++ b/static/js/pandora/infoView.indiancinema.js
@@ -0,0 +1,982 @@
+'use strict';
+
+pandora.ui.infoView = function(data) {
+
+ // fixme: given that currently, the info view doesn't scroll into view nicely
+ // when collapsing the movies browser, the info view should become a split panel
+
+ var ui = pandora.user.ui,
+ canEdit = pandora.site.capabilities.canEditMetadata[pandora.user.level],
+ css = {
+ marginTop: '4px',
+ textAlign: 'justify',
+ MozUserSelect: 'text',
+ WebkitUserSelect: 'text'
+ },
+ iconRatio = ui.icons == 'posters'
+ ? (ui.showSitePosters ? 5/8 : 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,
+ isEditable = canEdit && data.id.slice(0, 2) == '0x',
+ listWidth = 144 + Ox.UI.SCROLLBAR_SIZE,
+ margin = 16,
+ statisticsWidth = 128,
+ uid = Ox.uid(),
+
+ that = Ox.Element(),
+
+ $list,
+
+ $info = $('
')
+ .css({
+ position: 'absolute',
+ left: canEdit && !ui.showIconBrowser ? -listWidth + 'px' : 0,
+ top: 0,
+ right: 0
+ })
+ .appendTo(that.$element),
+
+ $data = Ox.Container()
+ .css({
+ position: 'absolute',
+ left: (canEdit ? listWidth : 0) + 'px',
+ top: 0,
+ right: 0,
+ height: pandora.$ui.contentPanel.size(1) + 'px'
+ })
+ .appendTo($info),
+
+ $icon = Ox.Element({
+ element: '
',
+ tooltip: canEdit ? (
+ !ui.showIconBrowser
+ ? 'Doubleclick to edit icon'
+ : 'Doubleclick to hide icons'
+ ) : ''
+ })
+ .attr({
+ src: '/' + data.id + '/' + (
+ ui.icons == 'posters'
+ ? (ui.showSitePosters ? 'siteposter' : 'poster') : 'icon'
+ ) + '512.jpg?' + uid
+ })
+ .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($data.$element),
+
+ $reflection = $('
')
+ .addClass('OxReflection')
+ .css({
+ position: 'absolute',
+ left: margin + 'px',
+ top: margin + iconHeight + 'px',
+ width: iconSize + 'px',
+ height: iconSize / 2 + 'px',
+ overflow: 'hidden'
+ })
+ .appendTo($data.$element),
+
+ $reflectionIcon = $('
')
+ .attr({
+ src: '/' + data.id + '/' + (
+ ui.icons == 'posters'
+ ? (ui.showSitePosters ? 'siteposter' : 'poster') : 'icon'
+ ) + '512.jpg?' + uid
+ })
+ .css({
+ position: 'absolute',
+ left: iconLeft + 'px',
+ width: iconWidth + 'px',
+ height: iconHeight + 'px',
+ borderRadius: borderRadius + 'px'
+ })
+ .appendTo($reflection),
+
+ $reflectionGradient = $('
')
+ .css({
+ position: 'absolute',
+ width: iconSize + 'px',
+ height: iconSize / 2 + 'px'
+ })
+ .appendTo($reflection),
+
+ $text = Ox.Element()
+ .css({
+ position: 'absolute',
+ left: margin + (iconSize == 256 ? 256 : iconWidth) + margin + 'px',
+ top: margin + 'px',
+ right: margin + statisticsWidth + margin + 'px'
+ })
+ .appendTo($data.$element),
+
+ $statistics = $('
')
+ .css({
+ position: 'absolute',
+ width: statisticsWidth + 'px',
+ top: margin + 'px',
+ right: margin + 'px'
+ })
+ .appendTo($data.$element),
+
+ $reloadButton,
+
+ $capabilities,
+
+ $browserImages = [];
+
+ pandora.createLinks($text); // FIXME: this is wrong for editables that already have clickLink
+
+ // Title -------------------------------------------------------------------
+
+ $('
')
+ .css({
+ marginTop: '-2px'
+ })
+ .append(
+ Ox.Editable({
+ clickLink: pandora.clickLink,
+ editable: isEditable,
+ format: function(value) {
+ return formatTitle(value);
+ },
+ tooltip: isEditable ? 'Doubleclick to edit' : '',
+ value: data.title
+ })
+ .css({
+ display: 'inline-block',
+ marginBottom: '-3px',
+ fontWeight: 'bold',
+ fontSize: '13px',
+ MozUserSelect: 'text',
+ WebkitUserSelect: 'text'
+ })
+ .bindEvent({
+ submit: function(event) {
+ editMetadata('title', event.value);
+ }
+ })
+ .appendTo($text)
+ )
+ .appendTo($text);
+
+ // Director ----------------------------------------------------------------
+
+ if (data.director || isEditable) {
+ $('
')
+ .css({
+ marginTop: '2px'
+ })
+ .append(
+ Ox.Editable({
+ clickLink: pandora.clickLink,
+ editable: isEditable,
+ format: function(value) {
+ return formatValue(value.split(', '), 'name');
+ },
+ placeholder: formatLight('Unknown Director'),
+ tooltip: isEditable ? 'Doubleclick to edit' : '',
+ value: data.director ? data.director.join(', ') : 'Unknown Director'
+ })
+ .css({
+ display: 'inline-block',
+ marginBottom: '-3px',
+ fontWeight: 'bold',
+ fontSize: '13px',
+ MozUserSelect: 'text',
+ WebkitUserSelect: 'text'
+ })
+ .bindEvent({
+ submit: function(event) {
+ editMetadata('director', event.value);
+ }
+ })
+ )
+ .appendTo($text);
+ }
+
+ // Country, Year, Language, Runtime ----------------------------------------
+
+ if (isEditable) {
+ var $div = $('
')
+ .css(css)
+ .appendTo($text);
+ ['country', 'year'].forEach(function(key, i) {
+ i && $('
').css({float: 'left'}).html('; ').appendTo($div);
+ $('
')
+ .css({float: 'left'})
+ .html(formatKey(key).replace('', ' '))
+ .appendTo($div);
+ Ox.Editable({
+ clickLink: pandora.clickLink,
+ format: function(value) {
+ return formatValue(value.split(', '), key)
+ },
+ placeholder: formatLight('unknown'),
+ tooltip: 'Doubleclick to edit',
+ value: key == 'country'
+ ? (data[key] ? data[key].join(', ') : [''])
+ : data[key] || ''
+ })
+ .css({float: 'left'})
+ .bindEvent({
+ submit: function(event) {
+ editMetadata(key, event.value);
+ }
+ })
+ .appendTo($div);
+ });
+ } else if (data.country || data.year || data.language || data.runtime) {
+ var html = [];
+ ['country', 'year', 'language', 'runtime'].forEach(function(key) {
+ if (data[key]) {
+ html.push(
+ formatKey(key) + (
+ key != 'runtime' ? formatValue(data[key], key)
+ : data[key] < 60 ? Math.round(data[key]) + ' sec'
+ : Math.round(data[key] / 60) + ' min'
+ )
+ )
+ }
+ });
+ $('
').css(css).html(html.join('; ')).appendTo($text);
+ }
+
+ // Alternative Titles ------------------------------------------------------
+
+ data.alternativeTitles && $('
')
+ .css(css)
+ .html(
+ formatKey('Alternative Title' + (data.alternativeTitles.length == 1 ? '' : 's'))
+ + data.alternativeTitles.map(function(value) {
+ return value[0] + (Ox.isArray(value[1]) ? ' '
+ + formatLight('(' + value[1].join(', ') + ')') : '');
+ }).join(', ')
+ )
+ .appendTo($text);
+
+ // FIXME: we will want to check for data.seriesId here
+ if (isEditable && data.seriesTitle) {
+ var $div = $('
')
+ .css(Ox.extend(css, {marginTop: '20px'})) // FIXME: just a guess
+ .appendTo($text);
+ ['episodeDirector', 'seriesYear'].forEach(function(key, i) {
+ i && $('
').css({float: 'left'}).html('; ').appendTo($div);
+ $('
')
+ .css({float: 'left'})
+ .html(formatKey(Ox.toUnderscores(key).replace(/_/g, ' ')).replace('', ' '))
+ .appendTo($div);
+ Ox.Editable({
+ clickLink: pandora.clickLink,
+ format: function(value) {
+ return formatValue(value.split(', '), key)
+ },
+ placeholder: formatLight('unknown'),
+ tooltip: 'Doubleclick to edit',
+ value: key == 'episodeDirector'
+ ? (data[key] ? data[key].join(', ') : [''])
+ : data[key] || ''
+ })
+ .css({float: 'left'})
+ .bindEvent({
+ submit: function(event) {
+ editMetadata(key, event.value);
+ }
+ })
+ .appendTo($div);
+ });
+ } else if (data.episodeDirector || data.writer || data.producer || data.cinematographer || data.editor) {
+ $div = $('
')
+ .css(css)
+ .appendTo($text);
+ html = [];
+ ['episodeDirector', 'writer', 'producer', 'cinematographer', 'editor'].forEach(function(key) {
+ data[key] && html.push(
+ formatKey(key == 'episodeDirector' ? 'director' : key) + formatValue(data[key], 'name')
+ );
+ });
+ $div.html(html.join('; '));
+ }
+
+ data.cast && $('
')
+ .css(css)
+ .html(
+ formatKey('cast') + data.cast.map(function(value) {
+ // FIXME: 'uncredited' should be removed on the backend
+ value.character = value.character.replace('(uncredited)', '').trim();
+ return formatValue(value.actor, 'name')
+ + (value.character ? ' '
+ + formatLight('(' + formatValue(value.character) + ')')
+ : '');
+ }).join(', ')
+ )
+ .appendTo($text);
+
+ if (data.genre || data.keyword) {
+ $div = $('
')
+ .css(css)
+ .appendTo($text);
+ html = [];
+ ['genre', 'keyword'].forEach(function(key) {
+ data[key] && html.push(
+ formatKey(key == 'keyword' ? 'keywords' : key)
+ + formatValue(data[key], key)
+ );
+ });
+ $div.html(html.join('; '));
+ }
+
+ data.summary && $('
')
+ .css(css)
+ .html(
+ formatKey('summary') + data.summary
+ )
+ .appendTo($text);
+
+ data.trivia && data.trivia.forEach(function(value) {
+ $('
')
+ .css({
+ display: 'table-row'
+ })
+ .append(
+ $('
')
+ .css({
+ display: 'table-cell',
+ width: '12px',
+ paddingTop: '4px'
+ })
+ .html('
•')
+ )
+ .append(
+ $('
')
+ .css({
+ display: 'table-cell',
+ paddingTop: '4px',
+ textAlign: 'justify',
+ MozUserSelect: 'text',
+ WebkitUserSelect: 'text'
+ })
+ .html(value)
+ )
+ .append(
+ $('
').css({clear: 'both'})
+ )
+ .appendTo($text);
+ });
+
+ data.filmingLocations && $('
')
+ .css(css)
+ .html(
+ formatKey('Filming Locations') + data.filmingLocations.map(function(location) {
+ return '
' + location + ''
+ }).join(', ')
+ )
+ .appendTo($text);
+
+ data.releasedate && $('
')
+ .css(css)
+ .html(
+ formatKey('Release Date') + Ox.formatDate(data.releasedate, '%A, %B %e, %Y')
+ )
+ .appendTo($text);
+
+ if (data.budget || data.gross || data.profit) {
+ $div = $('
')
+ .css(css)
+ .appendTo($text);
+ html = [];
+ ['budget', 'gross', 'profit'].forEach(function(key) {
+ data[key] && html.push(
+ formatKey(key == 'profit' && data[key] < 0 ? 'loss' : key)
+ + Ox.formatCurrency(Math.abs(data[key]), '$')
+ );
+ });
+ $div.html(html.join('; '));
+ }
+
+ if (data.connections) {
+ $div = $('
')
+ .css(css)
+ .appendTo($text);
+ html = [];
+ [
+ 'Edited from', 'Edited into',
+ 'Features', 'Featured in',
+ 'Follows', 'Followed by',
+ 'References', 'Referenced in',
+ 'Remake of', 'Remade as',
+ 'Spin off from', 'Spin off',
+ 'Spoofs', 'Spoofed in'
+ ].forEach(function(key) {
+ data.connections[key] && html.push(
+ formatKey(key) + data.connections[key].map(function(connection) {
+ return (
+ connection.item
+ ? '
' + connection.title + ''
+ : connection.title
+ ) + (
+ connection.description
+ ? ' ' + formatLight('(' + connection.description + ')')
+ : ''
+ );
+ }).join(', ')
+ );
+ });
+ $div.html(html.join('; '));
+ }
+
+ ['reviews', 'links'].forEach(function(key) {
+ data[key] && $('
')
+ .css(css)
+ .html(
+ formatKey(key) + data[key].map(function(value) {
+ return '
' + value.source + ''
+ }).join(', ')
+ )
+ .appendTo($text);
+ });
+
+ $('
').css({height: '16px'}).appendTo($text);
+
+ if (canEdit && !isEditable) {
+ $reloadButton = Ox.Button({
+ title: 'Reload Metadata',
+ width: 128
+ })
+ .css({marginBottom: '4px'})
+ .bindEvent({
+ click: reloadMetadata
+ })
+ .appendTo($statistics);
+ }
+
+ // Mainstream Score, Arthouse Score ----------------------------------------
+
+ ['votes', 'likes'].forEach(function(key) {
+ var value = data[key] || 0;
+ $('
')
+ .css({marginBottom: '4px'})
+ .append(
+ formatKey(
+ key == 'votes' ? 'Mainstream Score' : 'Arthouse Score', true
+ )
+ )
+ .append(
+ Ox.Theme.formatColorPercent(value, 1, true)
+ .css({textAlign: 'right'})
+ )
+ .appendTo($statistics);
+ });
+
+ // Duration, Aspect Ratio --------------------------------------------------
+
+ ['duration', 'aspectratio'].forEach(function(key) {
+ var itemKey = Ox.getObjectById(pandora.site.itemKeys, key),
+ value = data[key] || 0;
+ $('
')
+ .css({marginBottom: '4px'})
+ .append(formatKey(itemKey.title, true))
+ .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) {
+ var value = data[key] || 0;
+ $('
')
+ .css({marginBottom: '4px'})
+ .append(formatKey(key, true))
+ .append(
+ Ox.Theme.formatColor(value, key == 'volume' ? 'lightness' : key)
+ .css({textAlign: 'right'})
+ )
+ .appendTo($statistics);
+ });
+
+ // Cuts per Minute, Words per Minute ---------------------------------------
+
+ ['cutsperminute', 'wordsperminute'].forEach(function(key) {
+ var value = data[key] || 0;
+ $('
')
+ .css({marginBottom: '4px'})
+ .append(
+ formatKey(key.slice(0, -9) + ' per minute', true)
+ )
+ .append(
+ Ox.Theme.formatColor(null, 'gradient')
+ .css({textAlign: 'right'})
+ .html(Ox.formatNumber(value, 3))
+ )
+ .appendTo($statistics);
+ });
+
+ // Rights Level ------------------------------------------------------------
+
+ var $rightsLevel = $('
');
+ $('
')
+ .css({marginBottom: '4px'})
+ .append(formatKey('Rights Level', true))
+ .append($rightsLevel)
+ .appendTo($statistics);
+ renderRightsLevel();
+
+ // Notes -------------------------------------------------------------------
+
+ if (canEdit) {
+
+ $('
')
+ .css({marginBottom: '4px'})
+ .append(formatKey('Notes', true))
+ .append(
+ Ox.Editable({
+ height: 128,
+ placeholder: formatLight('No notes'),
+ tooltip: 'Doubleclick to edit',
+ type: 'textarea',
+ value: data.notes,
+ width: 128
+ })
+ .bindEvent({
+ submit: function(event) {
+ pandora.api.edit({
+ id: data.id,
+ notes: event.value
+ }, function(result) {
+ // ...
+ });
+ }
+ })
+ )
+ .appendTo($statistics);
+
+ }
+
+ $('
').css({height: '16px'}).appendTo($statistics);
+
+ if (canEdit) {
+ $icon.bindEvent({
+ doubleclick: function() {
+ pandora.UI.set('showIconBrowser', !ui.showIconBrowser);
+ $info.animate({
+ left: ui.showIconBrowser ? 0 : -listWidth + 'px'
+ }, 250);
+ $icon.options({
+ tooltip: !pandora.user.ui.showIconBrowser
+ ? 'Doubleclick to edit icon'
+ : 'Doubleclick to hide icons'
+ });
+ }
+ });
+ renderList();
+ }
+
+ function editMetadata(key, value) {
+ if (value != data[key]) {
+ var edit = {id: data.id};
+ if (key == 'title') {
+ Ox.extend(edit, parseTitle(value));
+ } else if (['director', 'country'].indexOf(key) > -1) {
+ edit[key] = value ? value.split(', ') : [];
+ } else {
+ edit[key] = value;
+ }
+ pandora.api.edit(edit, function(result) {
+ if (result.data.id != data.id) {
+ Ox.Request.clearCache(); // fixme: too much
+ pandora.UI.set({item: result.data.id});
+ pandora.$ui.browser.value(data.id, 'id', result.data.id);
+ }
+ // FIXME: value function should accept {k: v, ...}
+ pandora.$ui.browser.value(result.data.id, 'title', result.data.title);
+ pandora.$ui.browser.value(result.data.id, 'director', result.data.director);
+ pandora.$ui.browser.value(result.data.id, 'country', result.data.country);
+ pandora.$ui.browser.value(result.data.id, 'year', result.data.year);
+ //pandora.$ui.contentPanel.replaceElement(0, pandora.$ui.browser = pandora.ui.browser());
+ });
+ }
+ }
+
+ function formatKey(key, isStatistics) {
+ return isStatistics
+ ? $('
').css({marginBottom: '4px', fontWeight: 'bold'})
+ .html(Ox.toTitleCase(key).replace(' Per ', ' per '))
+ : '
' + Ox.toTitleCase(key) + ': ';
+ }
+
+ function formatLight(str) {
+ return '
' + str + '';
+ }
+
+ function formatTitle(title) {
+ var match = /(.+) (\(S\d{2}E\d{2}\))/.exec(title);
+ if (match) {
+ title = formatValue(match[1], 'title') + ' '
+ + formatLight(match[2])
+ + title.substr(match[0].length);
+ }
+ return title + (
+ data.originalTitle && data.originalTitle != title
+ ? ' ' + formatLight('(' + data.originalTitle + ')') : ''
+ );
+ }
+
+ function formatValue(value, key) {
+ return (Ox.isArray(value) ? value : [value]).map(function(value) {
+ return key ?
+ '
' + value + ''
+ : value;
+ }).join(', ');
+ }
+
+ function getRightsLevelElement(rightsLevel) {
+ return Ox.Theme.formatColorLevel(
+ rightsLevel,
+ pandora.site.rightsLevels.map(function(rightsLevel) {
+ return rightsLevel.name;
+ })
+ );
+ }
+
+ function parseTitle(title) {
+ var data = {title: title},
+ match = /(\(S(\d{2})E(\d{2})\))/.exec(title),
+ split;
+ if (match) {
+ data.season = parseInt(match[2], 10);
+ data.episode = parseInt(match[3], 10);
+ split = title.split(match[1]);
+ data.seriesTitle = split[0].trim();
+ data.episodeTitle = split[1].trim();
+ }
+ return data;
+ }
+
+ function reloadMetadata() {
+ var item = ui.item;
+ // fixme: maybe there's a better method name for this?
+ $reloadButton.options({disabled: true, title: 'Reloading Metadata'});
+ pandora.api.updateExternalData({
+ id: ui.item
+ }, function(result) {
+ //reloading metadata might change results too(i.e. genre)
+ Ox.Request.clearCache();
+ if (ui.item == item && ui.itemView == 'info') {
+ pandora.$ui.contentPanel.replaceElement(
+ 1, pandora.$ui.item = pandora.ui.item()
+ );
+ }
+ });
+ }
+
+ 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 = $('
')
+ .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()
+ .replace('see item', 'see the item')
+ .replace('play video', 'play the full video')
+ .replace('download video', 'download the video'),
+ 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: 'Help',
+ tooltip: 'About Rights',
+ type: 'image'
+ })
+ .css({marginLeft: '52px'})
+ .bindEvent({
+ click: function() {
+ pandora.UI.set({page: 'rights'});
+ }
+ })
+ .appendTo($line);
+ }
+ });
+ }
+
+ function renderList() {
+ pandora.api.get({
+ id: data.id,
+ keys: [ui.icons == 'posters' ? 'posters' : 'frames']
+ }, 0, function(result) {
+ var images = result.data[ui.icons == 'posters' ? 'posters' : 'frames'].map(function(image) {
+ return Ox.extend(image, {index: image.index.toString()});
+ }),
+ selectedImage = images.filter(function(image) {
+ return image.selected;
+ })[0];
+ $list = Ox.IconList({
+ defaultRatio: ui.icons == 'posters' || !data.stream ? 5/8 : data.stream.aspectratio,
+ fixedRatio: ui.icons == 'posters' || !data.stream ? false : data.stream.aspectratio,
+ item: function(data, sort, size) {
+ var ratio = data.width / data.height;
+ size = size || 128;
+ return {
+ height: ratio <= 1 ? size : size / ratio,
+ id: data['id'],
+ info: data.width + ' x ' + data.height + ' px',
+ title: ui.icons == 'posters' ? data.source : Ox.formatDuration(data.position),
+ url: data.url.replace('http://', '//'),
+ width: ratio >= 1 ? size : size * ratio
+ }
+ },
+ items: images,
+ keys: ui.icons == 'posters'
+ ? ['index', 'source', 'width', 'height', 'url']
+ : ['index', 'position', 'width', 'height', 'url'],
+ max: 1,
+ min: 1,
+ orientation: 'both',
+ // fixme: should never be undefined
+ selected: selectedImage ? [selectedImage['index']] : [],
+ size: 128,
+ sort: [{key: 'index', operator: '+'}],
+ unique: 'index'
+ })
+ .addClass('OxMedia')
+ .css({
+ display: 'block',
+ position: 'absolute',
+ left: 0,
+ top: 0,
+ width: listWidth + 'px',
+ height: pandora.$ui.contentPanel.size(1) + 'px'
+ })
+ .bindEvent({
+ select: function(event) {
+ var index = event.ids[0];
+ selectedImage = images.filter(function(image) {
+ return image.index == index;
+ })[0];
+ var imageRatio = selectedImage.width / selectedImage.height,
+ src = selectedImage.url.replace('http://', '//');
+ if ($browserImages.length == 0) {
+ $browserImages = pandora.$ui.browser.find('img[src*="/' + data.id + '/"]');
+ }
+ if (ui.icons == 'posters' && !ui.showSitePosters) {
+ $browserImages.each(function() {
+ var $this = $(this),
+ size = Math.max($this.width(), $this.height());
+ $this.attr({src: src});
+ ui.icons == 'posters' && $this.css(imageRatio < 1 ? {
+ width: Math.round(size * imageRatio) + 'px',
+ height: size + 'px'
+ } : {
+ width: size + 'px',
+ height: Math.round(size / imageRatio) + 'px'
+ });
+ });
+ $icon.attr({src: src});
+ $reflectionIcon.attr({src: src});
+ iconRatio = imageRatio;
+ iconSize = iconSize == 256 ? 512 : 256;
+ toggleIconSize();
+ }
+ pandora.api[ui.icons == 'posters' ? 'setPoster' : 'setPosterFrame'](Ox.extend({
+ id: data.id
+ }, ui.icons == 'posters' ? {
+ source: selectedImage.source
+ } : {
+ // fixme: api slightly inconsistent, this shouldn't be "position"
+ position: selectedImage.index
+ }), function() {
+ // fixme: update the info (video preview) frame as well
+ var src;
+ pandora.clearIconCache(data.id);
+ Ox.Request.clearCache();
+ if (ui.icons == 'frames') {
+ src = '/' + data.id + '/icon512.jpg?' + Ox.uid()
+ $icon.attr({src: src});
+ $reflectionIcon.attr({src: src});
+ }
+ $browserImages.each(function() {
+ $(this).attr({src: '/' + data.id + '/' + (
+ ui.icons == 'posters' ? 'poster' : 'icon'
+ ) + '128.jpg?' + Ox.uid()});
+ });
+ });
+ }
+ })
+ .appendTo($info);
+ $list.size();
+ });
+ }
+
+ 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 = $('
').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'
+ ? (ui.showSitePosters ? 'siteposter' : '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 ? 5/8 : data.posterRatio) : 1;
+ toggleIconSize();
+ pandora.user.level == 'admin' && $list.replaceWith($list = renderList());
+ };
+
+ that.resize = function() {
+ var height = pandora.$ui.contentPanel.size(1);
+ $list && $list.css({height: height + 'px'});
+ $data.css({height: height + 'px'});
+ };
+
+ that.bindEvent({
+ pandora_icons: that.reload,
+ pandora_showsiteposters: function() {
+ ui.icons == 'posters' && that.reload();
+ }
+ });
+
+ return that;
+
+}