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; + +}