PageRenderTime 54ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 1ms

/src/js/qd.js

http://queued.googlecode.com/
JavaScript | 8580 lines | 5871 code | 849 blank | 1860 comment | 845 complexity | 6a49530350732425e620f032ff3ebdd0 MD5 | raw file
Possible License(s): Apache-2.0, MIT, BSD-3-Clause, LGPL-2.0
  1. /*
  2. Copyright (c) 2004-2009, The Dojo Foundation All Rights Reserved.
  3. Available via Academic Free License >= 2.1 OR the modified BSD license.
  4. see: http://dojotoolkit.org/license for details
  5. */
  6. /*
  7. This is a compiled version of Dojo, built for deployment and not for
  8. development. To get an editable version, please visit:
  9. http://dojotoolkit.org
  10. for documentation and information on getting the source.
  11. */
  12. /*
  13. Copyright (c) 2004-2009, The Dojo Foundation All Rights Reserved.
  14. Available via Academic Free License >= 2.1 OR the modified BSD license.
  15. see: http://dojotoolkit.org/license for details
  16. */
  17. /*
  18. This is a compiled version of Dojo, built for deployment and not for
  19. development. To get an editable version, please visit:
  20. http://dojotoolkit.org
  21. for documentation and information on getting the source.
  22. */
  23. if(!dojo._hasResource["qd.services.util"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  24. dojo._hasResource["qd.services.util"] = true;
  25. dojo.provide("qd.services.util");
  26. (function(){
  27. var reHexEntity=/&#x([^;]+);/g,
  28. reDecEntity=/&#([^;]+);/g;
  29. dojo.mixin(qd.services.util, {
  30. // summary:
  31. // A set of utility methods used throughout the Queued service layers.
  32. prepare: function(/* Object */args, /* dojo.Deferred? */d){
  33. // summary:
  34. // Prepare any deferred (or create a new one) and set
  35. // up the callback/errback pair on it. args.result
  36. // is the callback, args.error is the errback. Used
  37. // primarily by the communication services (online/offline).
  38. var dfd = d || new dojo.Deferred();
  39. if(args.result){
  40. dfd.addCallback(function(data, ioArgs){
  41. args.result.call(args, data, ioArgs);
  42. });
  43. }
  44. if(args.error){
  45. dfd.addErrback(function(evt, ioArgs){
  46. args.error.call(args, evt, ioArgs);
  47. });
  48. }
  49. return dfd; // dojo.Deferred
  50. },
  51. mixin: function(/* Object */dest, /* Object */override){
  52. // summary:
  53. // Custom mixin function that is considered "additive", as
  54. // opposed to simply overriding.
  55. // description:
  56. // Custom mixin function to stamp the properties from override
  57. // onto dest without clobbering member objects as you would in
  58. // a shallow copy like dojo.mixin does; this isn't particularly
  59. // robust or fast, but it works for our title and queue item objects.
  60. //
  61. // The basic property handling rules are:
  62. // - null doesn't overwrite anything, ever
  63. // - scalars get overwritten by anything, including new scalars
  64. // - arrays get overwritten by longer arrays or by objects
  65. // - objects get merged by recursively calling mixin()
  66. for(k in override){
  67. if(override[k] === null || override[k] === undefined){ continue; }
  68. if(dojo.isArray(override[k])){
  69. if(dojo.isArray(dest[k])){ // the longest array wins!
  70. if(override[k].length > dest[k].length){
  71. dest[k] = override[k].slice(0); // make a copy of the override.
  72. }
  73. } else {
  74. if(!dojo.isObject(dest[k])){
  75. dest[k] = override[k].slice(0);
  76. }
  77. }
  78. }
  79. else if(dojo.isObject(override[k])){
  80. if(dest[k] !== null && dojo.isObject(dest[k])){
  81. dest[k] = qd.services.util.mixin(dest[k], override[k]);
  82. }else{
  83. dest[k] = qd.services.util.mixin({}, override[k]);
  84. }
  85. }
  86. else{
  87. if(dest[k] === null || (!dojo.isArray(dest[k]) && !dojo.isObject(dest[k]))){
  88. if(!dest[k]){
  89. dest[k] = override[k];
  90. } else if (dest[k] && override[k] && dest[k] != override[k]){
  91. dest[k] = override[k];
  92. }
  93. }
  94. }
  95. }
  96. return dest; // Object
  97. },
  98. clean: function(/* String */str){
  99. // summary:
  100. // Pull out any HTML tags and replace any HTML entities with the
  101. // proper characters. Used primarily for the description/synopsis
  102. // of a title coming from one of the Netflix public RSS feeds.
  103. return str.replace(reHexEntity, function(){ // String
  104. return String.fromCharCode(parseInt(arguments[1],16));
  105. })
  106. .replace(reDecEntity, function(){
  107. return String.fromCharCode(parseInt(arguments[1],10));
  108. })
  109. .replace(/\&quot\;/g, '"')
  110. .replace(/\&apos\;/g, "'")
  111. .replace(/\&amp\;/g, "&")
  112. .replace(/<[^>]*>/g, "");
  113. },
  114. image: {
  115. // summary:
  116. // Helper functions for caching images for offline.
  117. url: function(url){
  118. // summary:
  119. // Return the best url for the image.
  120. // url: String
  121. // The Netflix URL to check against the local cache.
  122. var file = air.File.applicationStorageDirectory.resolvePath(url.replace("http://", ""));
  123. if(file.exists){
  124. return file.url;
  125. }
  126. return url; // String
  127. },
  128. store: function(url){
  129. // summary:
  130. // Return the best url for the image.
  131. // url: String
  132. // The Netflix URL to store to the local cache.
  133. var l = new air.URLLoader(), u = new air.URLRequest(url);
  134. var dfd = new dojo.Deferred();
  135. l.dataFormat = air.URLLoaderDataFormat.BINARY;
  136. // save the data once it has completed loading.
  137. l.addEventListener(air.Event.COMPLETE, function(evt){
  138. // make sure the cache directory is created
  139. var tmpUrl = url.replace("http://", "");
  140. var file = air.File.applicationStorageDirectory.resolvePath(tmpUrl);
  141. // this branch shouldn't happen but just in case...
  142. if(file.exists){
  143. file.deleteFile();
  144. }
  145. // open up the file object for writing.
  146. var fs = new air.FileStream();
  147. fs.open(file, air.FileMode.WRITE);
  148. fs.writeBytes(l.data, 0, l.data.length);
  149. fs.close();
  150. // fire the callback
  151. dfd.callback(file.url, url);
  152. });
  153. // do something about an error
  154. l.addEventListener(air.IOErrorEvent.IO_ERROR, function(evt){
  155. dfd.errback(url, evt);
  156. });
  157. // just in case a security error is thrown.
  158. l.addEventListener(air.SecurityErrorEvent.SECURITY_ERROR, function(evt){
  159. dfd.errback(url, evt);
  160. });
  161. // load the URL.
  162. l.load(u);
  163. return dfd; // dojo.Deferred
  164. }
  165. }
  166. });
  167. })();
  168. }
  169. if(!dojo._hasResource["qd.services.storage"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  170. dojo._hasResource["qd.services.storage"] = true;
  171. dojo.provide("qd.services.storage");
  172. (function(){
  173. var els = air.EncryptedLocalStore,
  174. ba = air.ByteArray;
  175. qd.services.storage = new (function(/* Boolean? */useCompression){
  176. // summary:
  177. // A singleton object that acts as the broker to the Encrypted Local Storage of AIR.
  178. var compress = useCompression || false;
  179. // basic common functionality
  180. this.item = function(/* String */key, /* Object? */value){
  181. // summary:
  182. // Provide a dojo-like interface for getting and
  183. // setting items in the Store.
  184. if(key === null || key === undefined || !key.length){
  185. throw new Error("qd.services.storage.item: you cannot pass an undefined or empty string as a key.");
  186. }
  187. if(value !== undefined){
  188. // setter branch
  189. var stream = new ba();
  190. stream.writeUTFBytes(dojo.toJson(value));
  191. if(compress){
  192. stream.compress();
  193. }
  194. els.setItem(key, stream);
  195. return value; // Object
  196. }
  197. // getter branch
  198. var stream = els.getItem(key);
  199. if(!stream){
  200. return null; // Object
  201. }
  202. if(compress){
  203. try {
  204. stream.uncompress();
  205. } catch(ex){
  206. // odds are we have an uncompressed thing here, so simply kill it and return null.
  207. els.removeItem(key);
  208. return null; // Object
  209. }
  210. }
  211. // just in case, we make sure there's no "undefined" in the pulled JSON.
  212. var s = stream.readUTFBytes(stream.length).replace("undefined", "null");
  213. return dojo.fromJson(s); // Object
  214. };
  215. this.remove = function(/* String */key){
  216. // summary:
  217. // Remove the item at key from the Encrypted Local Storage.
  218. if(key === null || key === undefined || !key.length){
  219. throw new Error("qd.services.storage.remove: you cannot pass an undefined or empty string as a key.");
  220. }
  221. els.removeItem(key);
  222. };
  223. this.clear = function(){
  224. // summary:
  225. // Clear out anything in the Encryped Local Storage.
  226. els.reset();
  227. this.onClear();
  228. };
  229. this.onClear = function(){
  230. // summary:
  231. // Stub function to run anything when the storage is cleared.
  232. };
  233. })();
  234. })();
  235. }
  236. if(!dojo._hasResource["qd.services.data"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  237. dojo._hasResource["qd.services.data"] = true;
  238. dojo.provide("qd.services.data");
  239. qd.services.data = new (function(){
  240. // summary:
  241. // A singleton object that handles any interaction with the
  242. // encrypted database.
  243. // database: String
  244. // The filename of the database.
  245. this._testKey = "h1dd3n!!11one1";
  246. this._testDb = "queued-test.db"; // revert to queued.db
  247. var _key = this._testKey;
  248. this.database = this._testDb;
  249. var initFile = "js/updates/resources/initialize.sql",
  250. initialized = false;
  251. var syncConn = new air.SQLConnection(),
  252. asyncConn = new air.SQLConnection(),
  253. inCreation = false,
  254. self = this;
  255. // Properties
  256. this.__defineGetter__("initialized", function(){
  257. // summary:
  258. // Returns whether the engine is initialized.
  259. return initialized; // Boolean
  260. });
  261. // these can't be getters, unfortunately.
  262. this.connection = function(/* Boolean? */async){
  263. // summary:
  264. // Return the proper connection.
  265. return (async ? asyncConn : syncConn); // air.SQLConnection
  266. };
  267. this.connected = function(/* Boolean? */async){
  268. // summary:
  269. // Returns whether the appropriate connection is actually connected.
  270. return (async ? asyncConn.connected : syncConn.connected ); // Boolean
  271. };
  272. this.transacting = function(/* Boolean? */async){
  273. // summary:
  274. // Return whether the appropriate connection is in the middle of a transaction.
  275. return (async ? asyncConn.inTransaction : syncConn.inTransaction ); // Boolean
  276. };
  277. this.lastId = function(/* Boolean? */async){
  278. // summary:
  279. // Return the lastId of the appropriate connection (INSERT/REPLACE).
  280. return (async ? asyncConn.lastInsertRowID : syncConn.lastInsertRowID ); // mixed
  281. };
  282. function eventSetup(/* air.SQLConnection */conn){
  283. // set up all of our event handlers on the passed connection
  284. // open the db, set up the connection handlers
  285. conn.addEventListener(air.SQLEvent.OPEN, self.onOpen);
  286. conn.addEventListener(air.SQLErrorEvent.ERROR, self.onError);
  287. conn.addEventListener(air.SQLEvent.CLOSE, self.onClose);
  288. conn.addEventListener(air.SQLEvent.ANALYZE, self.onAnalyze);
  289. conn.addEventListener(air.SQLEvent.DEANALYZE, self.onDeanalyze);
  290. conn.addEventListener(air.SQLEvent.COMPACT, self.onCompact);
  291. conn.addEventListener(air.SQLEvent.BEGIN, self.onBegin);
  292. conn.addEventListener(air.SQLEvent.COMMIT, self.onCommit);
  293. conn.addEventListener(air.SQLEvent.ROLLBACK, self.onRollback);
  294. conn.addEventListener(air.SQLEvent.CANCEL, self.onCancel);
  295. conn.addEventListener(air.SQLUpdateEvent.INSERT, self.onInsert);
  296. conn.addEventListener(air.SQLUpdateEvent.UPDATE, self.onUpdate);
  297. conn.addEventListener("delete", self.onDelete);
  298. return conn;
  299. }
  300. this.init = function(/* String */key, /* String */db, /* Boolean? */forceCreate){
  301. // summary:
  302. // Initialize the Queued data service.
  303. // set up the key
  304. var k = key||this._testKey;
  305. if(typeof(k) == "string"){
  306. _key = new air.ByteArray();
  307. _key.writeUTFBytes(k);
  308. k = _key;
  309. }
  310. if(key){ this._testKey = key; }
  311. this.database = db || this.database;
  312. // open the sync connection and test to see if it needs to run the create statements
  313. var sync = eventSetup(syncConn);
  314. sync.open(air.File.applicationStorageDirectory.resolvePath(this.database), "create", false, 1024, k);
  315. // open the async connection
  316. var async = eventSetup(asyncConn);
  317. async.openAsync(air.File.applicationStorageDirectory.resolvePath(this.database), "create", null, false, 1024, k);
  318. if(!forceCreate){
  319. var s = new air.SQLStatement();
  320. s.sqlConnection = sync;
  321. // latest change: remove integer ID from Title table. If it exists, recreate.
  322. s.text = "SELECT json FROM Title LIMIT 1";
  323. try{
  324. s.execute();
  325. } catch(e){
  326. this.create({ connection: sync, file: this.initFile });
  327. }
  328. } else {
  329. this.create({ connection: sync, file: this.initFile });
  330. }
  331. this.onInitialize();
  332. // attach to app.onExit
  333. var h = dojo.connect(qd.app, "onExit", function(evt){
  334. if(evt.isDefaultPrevented()){
  335. return;
  336. }
  337. dojo.disconnect(h);
  338. async.close();
  339. sync.close();
  340. air.trace("Database connections closed.");
  341. });
  342. };
  343. /*=====
  344. qd.services.data.__CreateArgs = function(file, connection){
  345. // summary:
  346. // Optional keyword arguments object for the create method.
  347. // file: String?
  348. // The filename to be used for creating the db.
  349. // connection: air.SQLConnection?
  350. // The connection to be used for creating the db. Defaults
  351. // to the synchronous connection.
  352. }
  353. =====*/
  354. this.create = function(/* qd.services.data.__CreateArgs */kwArgs){
  355. // summary:
  356. // Create the database.
  357. inCreation = true;
  358. var file = kwArgs && kwArgs.file || initFile,
  359. conn = kwArgs && kwArgs.connection || syncConn;
  360. var f = air.File.applicationDirectory.resolvePath(file);
  361. if(f.exists){
  362. // kill off the async connection first.
  363. asyncConn.close();
  364. asyncConn = null;
  365. var fs = new air.FileStream();
  366. fs.open(f, air.FileMode.READ);
  367. var txt = fs.readUTFBytes(fs.bytesAvailable);
  368. fs.close();
  369. var st = new Date();
  370. // break it apart.
  371. txt = txt.replace(/\t/g, "");
  372. var c="", inMerge = false, test = txt.split(/\r\n|\r|\n/), a=[];
  373. for(var i=0; i<test.length; i++){
  374. if(inMerge){
  375. c += test[i];
  376. if(test[i].indexOf(")")>-1){
  377. a.push(c);
  378. c = "";
  379. inMerge = false;
  380. }
  381. } else {
  382. if(test[i].indexOf("(")>-1 && test[i].indexOf(")")==-1){
  383. inMerge = true;
  384. c += test[i];
  385. } else {
  386. a.push(test[i]);
  387. }
  388. }
  389. }
  390. // use raw SQL statements here because of the need to preempt any
  391. // statements that might have been called while creating.
  392. for(var i=0, l=a.length; i<l; i++){
  393. var item = dojo.trim(a[i]);
  394. if(!item.length || item.indexOf("--")>-1){ continue; }
  395. var s = new air.SQLStatement();
  396. s.text = item;
  397. s.sqlConnection = conn;
  398. s.execute();
  399. }
  400. // profiling
  401. console.warn("db creation took " + (new Date().valueOf() - st.valueOf()) + "ms.");
  402. // re-open the async connection.
  403. asyncConn = new air.SQLConnection();
  404. var async = eventSetup(asyncConn);
  405. async.openAsync(air.File.applicationStorageDirectory.resolvePath(this.database), "create", null, false, 1024, _key);
  406. // fire off the onCreate event.
  407. this.onCreate();
  408. // run an analysis on it
  409. //conn.analyze();
  410. }
  411. };
  412. /*=====
  413. qd.services.data.fetch.__Args = function(sql, params, result, error){
  414. // sql: String
  415. // The SQL statement to be executed.
  416. // params: Object|Array?
  417. // Any parameters to be pushed into the SQL statement. If an
  418. // Array, expects the SQL statement to be using ?, if an object
  419. // it expects the SQL statement to be using keywords, prepended
  420. // with ":".
  421. // result: Function?
  422. // The callback to be executed when results are returned.
  423. // error: Function?
  424. // The callback to be executed when an error occurs.
  425. this.sql = sql;
  426. this.params = params;
  427. this.result = result;
  428. this.error = error;
  429. }
  430. =====*/
  431. function prep(/* qd.services.data.fetch.__Args */kwArgs, /* air.SQLStatement */s, /* Boolean */async){
  432. // summary:
  433. // Prepare the SQL statement and return it.
  434. s.sqlConnection = kwArgs.connection || (async ? asyncConn : syncConn);
  435. s.text = kwArgs.sql;
  436. if(kwArgs.params && dojo.isArray(kwArgs.params)){
  437. // allow the ordered list version
  438. for(var i=0, l=kwArgs.params.length; i<l; i++){
  439. s.parameters[i] = kwArgs.params[i];
  440. }
  441. } else {
  442. var params = kwArgs.params || {};
  443. for(var p in params){
  444. s.parameters[":" + p] = params[p];
  445. }
  446. }
  447. return s; // air.SQLStatement
  448. }
  449. var queue = [], createHandler;
  450. function exec(){
  451. var o = queue.shift();
  452. if(o){
  453. o.deferred.addCallback(exec);
  454. o.deferred.addErrback(exec);
  455. o.statement.execute();
  456. }
  457. }
  458. function query(/* qd.services.data.fetch.__Args */kwArgs, /* air.SQLStatement */s){
  459. // summary:
  460. // Inner function to communicate with the database.
  461. // set up the deferred.
  462. var dfd = new dojo.Deferred();
  463. // set up the event handlers.
  464. var onResult = function(evt){
  465. var result = s.getResult();
  466. dfd.callback(result);
  467. };
  468. var onError = function(evt){
  469. console.warn(evt);
  470. dfd.errback(evt);
  471. }
  472. if(kwArgs.result){
  473. dfd.addCallback(function(result){
  474. kwArgs.result.call(kwArgs, result.data, result);
  475. });
  476. }
  477. if(kwArgs.error){
  478. dfd.addErrback(function(evt){
  479. kwArgs.error.call(kwArgs, evt);
  480. });
  481. }
  482. s.addEventListener(air.SQLEvent.RESULT, onResult);
  483. s.addEventListener(air.SQLErrorEvent.ERROR, onError);
  484. queue.push({
  485. statement: s,
  486. deferred: dfd
  487. });
  488. if(!inCreation){
  489. exec();
  490. }
  491. else if(!createHandler){
  492. // we only want this to start once, don't go adding a bunch more connections
  493. createHandler = dojo.connect(self, "onCreate", function(){
  494. dojo.disconnect(createHandler);
  495. createHandler = null;
  496. exec();
  497. });
  498. }
  499. return dfd; // dojo.Deferred
  500. }
  501. this.fetch = function(/* qd.services.data.fetch.__Args */kwArgs){
  502. // summary:
  503. // Fetch (i.e. read) data out of the database. Can be used for write operations
  504. // but is not recommended; use execute for write ops. This method is hard-coded
  505. // to use the synchronous connection (i.e. thread-blocking).
  506. if(!kwArgs.sql){
  507. console.log("qd.services.data.fetch: no SQL passed. " + dojo.toJson(kwArgs));
  508. return null;
  509. }
  510. // fetch should use the sync connection unless an SQLConnection is passed with the kwArgs.
  511. var s = prep(kwArgs, new air.SQLStatement(), false),
  512. d = query(kwArgs, s);
  513. this.onFetch(kwArgs);
  514. return d; // dojo.Deferred
  515. };
  516. this.execute = function(/* qd.services.data.fetch.__Args */kwArgs){
  517. // summary:
  518. // Execute the passed SQL against the database. Should be used
  519. // for write operations (INSERT, REPLACE, DELETE, UPDATE). This
  520. // method is hard-coded to use the asynchronous connection.
  521. if(!kwArgs.sql){
  522. console.log("qd.services.data.execute: no SQL passed. " + dojo.toJson(kwArgs));
  523. return null;
  524. }
  525. // execute should use the async connection unless an SQLConnection is passed with the kwArgs.
  526. var s = prep(kwArgs, new air.SQLStatement(), true),
  527. d = query(kwArgs, s);
  528. this.onExecute(kwArgs);
  529. return d; // dojo.Deferred
  530. };
  531. // event stubs
  532. this.onError = function(/* air.Event */evt){ };
  533. this.onOpen = function(/* air.Event */evt){ };
  534. this.onClose = function(/* air.Event */evt){ };
  535. // analysis & maintenance
  536. this.onAnalyze = function(/* air.Event */evt){ };
  537. this.onDeanalyze = function(/* air.Event */evt){ };
  538. this.onCompact = function(/* air.Event */evt){ };
  539. this.onInitialize = function(){ };
  540. this.onCreate = function(){
  541. inCreation = false;
  542. };
  543. // adding other database files
  544. this.onAttach = function(/* air.Event */evt){ };
  545. this.onDetach = function(/* air.Event */evt){ };
  546. // transactions
  547. this.onBegin = function(/* air.Event */evt){ };
  548. this.onCommit = function(/* air.Event */evt){ };
  549. this.onRollback = function(/* air.Event */evt){ };
  550. // SQL execution
  551. this.onFetch = function(/* qd.services.data.fetch.__Args */kwArgs){ };
  552. this.onExecute = function(/* qd.services.data.fetch.__Args */kwArgs){ };
  553. this.onCancel = function(/* air.Event */evt){ };
  554. this.onInsert = function(/* air.Event */evt){ };
  555. this.onUpdate = function(/* air.Event */evt){ };
  556. this.onDelete = function(/* air.Event */evt){ };
  557. })();
  558. }
  559. if(!dojo._hasResource["qd.services.network"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  560. dojo._hasResource["qd.services.network"] = true;
  561. dojo.provide("qd.services.network");
  562. (function(){
  563. var monitor, monitorUrl="http://www.netflix.com";
  564. qd.services.network = new (function(){
  565. // summary:
  566. // A singleton object for access to the network layer of Queued.
  567. var self = this,
  568. pollInterval = 2500;
  569. var statusChange = function(e){
  570. self.onChange((monitor && monitor.available));
  571. };
  572. // Properties
  573. this.__defineGetter__("isRunning", function(){
  574. // summary:
  575. // Return whether or not the monitor is running.
  576. return (monitor && monitor.running); // Boolean
  577. });
  578. this.__defineGetter__("lastPoll", function(){
  579. // summary:
  580. // Return the last time the monitor checked the network status.
  581. return (monitor && monitor.lastStatusUpdate); // Date
  582. });
  583. this.__defineGetter__("available", function(){
  584. // summary:
  585. // Return whether or not the network is available.
  586. return monitor && monitor.available; // Boolean
  587. });
  588. this.__defineSetter__("available", function(/* Boolean */b){
  589. // summary:
  590. // Explicitly set the network availability
  591. if(monitor){
  592. monitor.available = b;
  593. }
  594. return (monitor && monitor.available); // Boolean
  595. });
  596. // FIXME: This is for DEV purposes only!
  597. this.offline = function(){
  598. monitor.stop();
  599. monitor.available = false;
  600. };
  601. this.online = function(){
  602. monitor.start();
  603. };
  604. this.initialized = false;
  605. // Methods
  606. this.init = function(/* String? */url){
  607. // summary:
  608. // Initialize the network services by creating and starting the monitor.
  609. // set up the offline monitor
  610. monitor = new air.URLMonitor(new air.URLRequest((url||monitorUrl)));
  611. monitor.pollInterval = pollInterval;
  612. monitor.addEventListener(air.StatusEvent.STATUS, statusChange);
  613. self.initialized = true;
  614. self.onInitialize(monitor.urlRequest.url);
  615. };
  616. this.start = function(){
  617. // summary:
  618. // Start the monitor services.
  619. if(!monitor){
  620. self.init();
  621. }
  622. console.log("qd.services.network.start: monitor is running.");
  623. self.onStart();
  624. return monitor.start();
  625. };
  626. this.stop = function(){
  627. // summary:
  628. // Stop the monitor services.
  629. console.log("qd.services.network.stop: monitor is stopped.");
  630. self.onStop();
  631. return (monitor && monitor.stop());
  632. };
  633. // Event stubs
  634. this.onInitialize = function(/* String */url){
  635. // summary:
  636. // Fires when the network services is initialized.
  637. qd.app.splash("Network services initialized");
  638. };
  639. this.onStart = function(){
  640. // summary:
  641. // Fires when the network services is started.
  642. qd.app.splash("Network services started");
  643. };
  644. this.onStop = function(){
  645. // summary:
  646. // Fires when the network services is stopped.
  647. };
  648. this.onChange = function(/* Boolean */isAvailable){
  649. // summary:
  650. // Stub event to connect to when the network status changes
  651. console.log("qd.services.network.onChange: current status is " + isAvailable);
  652. };
  653. })();
  654. })();
  655. }
  656. if(!dojo._hasResource["qd.services.parser"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  657. dojo._hasResource["qd.services.parser"] = true;
  658. dojo.provide("qd.services.parser");
  659. (function(){
  660. // summary:
  661. // A set of objects used to parse any XML returned by the Netflix API.
  662. var util = qd.services.util;
  663. // TITLES
  664. var baseTitle = {
  665. guid: null,
  666. rentalHistoryGuid: null,
  667. atHomeGuid: null,
  668. recommendationGuid: null,
  669. ratingGuid: null,
  670. type: null,
  671. title: null,
  672. art:{
  673. small: null,
  674. medium: null,
  675. large: null
  676. },
  677. releaseYear: null,
  678. runTime: null,
  679. rating: null,
  680. synopsis: null,
  681. ratings:{ average:null, predicted:null, user:null },
  682. categories: [],
  683. bonusMaterials: [],
  684. awards: {
  685. nominee:[],
  686. winner:[]
  687. },
  688. formats:{ },
  689. screenFormats: [],
  690. cast: [],
  691. directors: [],
  692. series: {},
  693. season: {},
  694. similars: [],
  695. audio: {},
  696. dates: {
  697. updated: null,
  698. availability: null
  699. },
  700. discs: [],
  701. episodes: [],
  702. fullDetail: false
  703. };
  704. // Parse helpers.
  705. function parseAwards(nl){
  706. var ret = [];
  707. for(var i=0,l=nl.length; i<l; i++){
  708. var n = nl[i];
  709. var tmp = {
  710. year: n.getAttribute("year")
  711. };
  712. var c = n.childNodes;
  713. for(var j=0, jl=c.length; j<jl; j++){
  714. var cn = c[j];
  715. if(cn.nodeType==1){
  716. if(cn.tagName=="category"){
  717. tmp.scheme = cn.getAttribute("scheme");
  718. tmp.term = cn.getAttribute("term");
  719. }
  720. else if(cn.tagName=="link"){
  721. var guid = cn.getAttribute("href");
  722. tmp.person = {
  723. guid: guid,
  724. id: guid.substr(guid.lastIndexOf("/")+1),
  725. title: cn.getAttribute("title")
  726. };
  727. }
  728. }
  729. }
  730. ret.push(tmp);
  731. }
  732. return ret;
  733. }
  734. function parseFormats(nl){
  735. var ret = {};
  736. for(var i=0, l=nl.length; i<l; i++){
  737. var available = nl[i].getAttribute("available_from");
  738. var d = parseInt(available+"000", 10);
  739. var n = nl[i].getElementsByTagName("category")[0];
  740. if(n){
  741. ret[n.getAttribute("term")] = (available)?new Date(d):null;
  742. }
  743. }
  744. return ret;
  745. }
  746. function parseDate(tenDijitStr){
  747. return dojo.date.locale.format(new Date(Number(tenDijitStr+"000")), {selector:"date", datePattern:"MM/dd/yy"});
  748. }
  749. function parseScreenFormats(nl){
  750. var ret = [];
  751. for(var i=0, l=nl.length; i<l; i++){
  752. var categories = nl[i].getElementsByTagName("category"),
  753. info = { title:"", screen:"" };
  754. for(var j=0, jl=categories.length; j<jl; j++){
  755. var c = categories[j];
  756. if(c.getAttribute("scheme").indexOf("title")>-1){
  757. info.title = c.getAttribute("term");
  758. } else {
  759. info.screen = c.getAttribute("term");
  760. }
  761. }
  762. ret.push(info);
  763. }
  764. return ret;
  765. }
  766. function parseLinks(nl){
  767. var ret = [];
  768. for(var i=0, l=nl.length; i<l; i++){
  769. var guid = nl[i].getAttribute("href");
  770. ret.push({
  771. guid: guid,
  772. title: nl[i].getAttribute("title")
  773. });
  774. }
  775. return ret;
  776. }
  777. function parseAudio(nl){
  778. var ret = {};
  779. for(var i=0, l=nl.length; i<l; i++){
  780. var node = nl[i], tfnode;
  781. for(var j=0; j<node.childNodes.length; j++){
  782. if(node.childNodes[j].nodeType != 1){ continue; }
  783. tfnode = node.childNodes[j];
  784. break;
  785. }
  786. var tf = tfnode.getAttribute("term");
  787. ret[tf]={ };
  788. for(var j=0; j<tfnode.childNodes.length; j++){
  789. if(tfnode.childNodes[j].nodeType != 1){ continue; }
  790. var lnode = tfnode.childNodes[j],
  791. tmp = [],
  792. lang = tfnode.childNodes[j].getAttribute("term");
  793. for(var k=0; k<lnode.childNodes.length; k++){
  794. if(lnode.childNodes[k].nodeType != 1){ continue; }
  795. tmp.push(lnode.childNodes[k].getAttribute("term"));
  796. }
  797. ret[tf][lang] = tmp;
  798. }
  799. }
  800. return ret;
  801. }
  802. // public functions
  803. var p = qd.services.parser;
  804. p.titles = {
  805. // summary:
  806. // The XML parser for any title information (movie, TV show, etc.)
  807. fromRss: function(/* XmlNode */node, /* Object? */obj){
  808. // summary:
  809. // Parse basic movie information from the passed RSS element.
  810. var o = dojo.clone(baseTitle);
  811. for(var i=0, l=node.childNodes.length; i<l; i++){
  812. var n=node.childNodes[i];
  813. if(n.nodeType != 1){ continue; } // ignore non-elements
  814. switch(n.tagName){
  815. case "title":
  816. var pieces = dojo.trim(n.textContent).match(/^\d+-\s(.*)$/);
  817. o.title = pieces ? pieces[1]: dojo.trim(n.textContent);
  818. break;
  819. case "guid":
  820. /*
  821. // TODO: TV series/seasons detection.
  822. var guid = dojo.trim(n.textContent);
  823. // swap out their ID for the real one.
  824. o.id = guid.substr(guid.lastIndexOf("/")+1);
  825. o.guid = "http://api.netflix.com/catalog/titles/movies/" + o.id;
  826. */
  827. break;
  828. case "description":
  829. var pieces = dojo.trim(n.textContent).match(/<img src="([^"]+)".*<br>([^$]*)/);
  830. if(pieces){
  831. o.art.small = pieces[1].replace("/small/", "/tiny/");
  832. o.art.medium = pieces[1];
  833. o.art.large = pieces[1].replace("/small/", "/large/");
  834. o.synopsis = util.clean(pieces[2]);
  835. }
  836. break;
  837. }
  838. }
  839. if(obj){
  840. o = util.mixin(obj, o);
  841. }
  842. return o; // Object
  843. },
  844. fromXml: function(/* XmlNode */node, /* Object?*/obj){
  845. // summary:
  846. // Parse the returned title information from the passed XmlNode.
  847. var o = dojo.clone(baseTitle);
  848. var links = node.ownerDocument.evaluate("./link", node),
  849. info = node.ownerDocument.evaluate("./*[name()!='link']", node),
  850. currentNode;
  851. while(currentNode = info.iterateNext()){
  852. switch(currentNode.tagName){
  853. case "id":
  854. // need to fork this a little because of "other" ids that are
  855. // possibly passed by Netflix.
  856. var test = currentNode.parentNode.tagName,
  857. value = dojo.trim(currentNode.textContent);
  858. if(test == "ratings_item"){
  859. o.ratingGuid = value;
  860. }
  861. else if(test == "rental_history_item"){
  862. o.rentalHistoryGuid = value;
  863. }
  864. else if(test == "at_home_item"){
  865. o.atHomeGuid = value;
  866. }
  867. else if(test == "recommendation"){
  868. o.recommendationGuid = value;
  869. }
  870. else {
  871. o.guid = value;
  872. }
  873. break;
  874. case "title":
  875. o.title = currentNode.getAttribute("regular");
  876. break;
  877. case "box_art":
  878. o.art = {
  879. small: currentNode.getAttribute("small"),
  880. medium: currentNode.getAttribute("medium"),
  881. large: currentNode.getAttribute("large")
  882. };
  883. break;
  884. case "release_year":
  885. o.releaseYear = dojo.trim(currentNode.textContent);
  886. break;
  887. case "runtime":
  888. o.runTime = parseInt(dojo.trim(currentNode.textContent), 10)/60;
  889. break;
  890. case "category":
  891. var scheme = currentNode.getAttribute("scheme");
  892. scheme = scheme.substr(scheme.lastIndexOf("/")+1);
  893. if(scheme == "mpaa_ratings" || scheme == "tv_ratings"){
  894. o.rating = currentNode.getAttribute("term");
  895. }
  896. else if (scheme == "genres"){
  897. o.categories.push(currentNode.getAttribute("term"));
  898. }
  899. break;
  900. case "user_rating":
  901. var val = currentNode.getAttribute("value");
  902. if(val == "not_interested"){
  903. o.ratings.user = val;
  904. }else{
  905. o.ratings.user = parseFloat(dojo.trim(currentNode.textContent), 10);
  906. }
  907. break;
  908. case "predicted_rating":
  909. o.ratings.predicted = parseFloat(dojo.trim(currentNode.textContent), 10);
  910. break;
  911. case "average_rating":
  912. o.ratings.average = parseFloat(dojo.trim(currentNode.textContent), 10);
  913. break;
  914. case "availability_date":
  915. o.dates.availability = parseDate(currentNode.textContent);
  916. break;
  917. case "updated":
  918. o.dates.updated = parseDate(currentNode.textContent);
  919. break;
  920. }
  921. }
  922. // do the links now.
  923. while(currentNode = links.iterateNext()){
  924. var type = currentNode.getAttribute("title"),
  925. rel = currentNode.getAttribute("rel");
  926. switch(rel){
  927. case "http://schemas.netflix.com/catalog/titles/synopsis":
  928. o.synopsis = util.clean(dojo.trim(currentNode.textContent));
  929. break;
  930. case "http://schemas.netflix.com/catalog/titles/awards":
  931. o.awards.nominee=parseAwards(currentNode.getElementsByTagName("award_nominee"));
  932. o.awards.winner=parseAwards(currentNode.getElementsByTagName("award_winner"));
  933. break;
  934. case "http://schemas.netflix.com/catalog/titles/format_availability":
  935. var nodes = currentNode.getElementsByTagName("availability");
  936. if(nodes && nodes.length){
  937. o.formats = parseFormats(nodes);
  938. }
  939. break;
  940. case "http://schemas.netflix.com/catalog/titles/screen_formats":
  941. o.screenFormats = parseScreenFormats(currentNode.getElementsByTagName("screen_format"));
  942. break;
  943. case "http://schemas.netflix.com/catalog/people.cast":
  944. o.cast = parseLinks(currentNode.getElementsByTagName("link"));
  945. break;
  946. case "http://schemas.netflix.com/catalog/people.directors":
  947. o.directors = parseLinks(currentNode.getElementsByTagName("link"));
  948. break;
  949. case "http://schemas.netflix.com/catalog/titles/languages_and_audio":
  950. o.audio = parseAudio(currentNode.getElementsByTagName("language_audio_format"));
  951. break;
  952. case "http://schemas.netflix.com/catalog/titles.similars":
  953. o.similars = parseLinks(currentNode.getElementsByTagName("link"));
  954. break;
  955. case "http://schemas.netflix.com/catalog/titles/bonus_materials":
  956. o.bonusMaterials = parseLinks(currentNode.getElementsByTagName("link"));
  957. break;
  958. case "http://schemas.netflix.com/catalog/titles/official_url":
  959. break;
  960. case "http://schemas.netflix.com/catalog/title":
  961. o.guid = currentNode.getAttribute("href");
  962. o.title = type;
  963. break;
  964. case "http://schemas.netflix.com/catalog/titles.series":
  965. o.series = {
  966. guid: currentNode.getAttribute("href"),
  967. title: type
  968. };
  969. break;
  970. case "http://schemas.netflix.com/catalog/titles.season":
  971. o.season = {
  972. guid: currentNode.getAttribute("href"),
  973. title: type
  974. };
  975. break;
  976. case "http://schemas.netflix.com/catalog/titles.discs":
  977. dojo.query("link", currentNode).forEach(function(disc){
  978. o.discs.push({
  979. guid: disc.getAttribute("href"),
  980. title: disc.getAttribute("title")
  981. });
  982. });
  983. break;
  984. case "http://schemas.netflix.com/catalog/titles.programs":
  985. dojo.query("link", currentNode).forEach(function(episode){
  986. o.episodes.push({
  987. guid: episode.getAttribute("href"),
  988. title: episode.getAttribute("title")
  989. });
  990. });
  991. break;
  992. }
  993. }
  994. if(obj){
  995. o = util.mixin(obj, o);
  996. }
  997. this.setType(o);
  998. o.fullDetail = true; // we have the full details now, so mark it as such.
  999. return o; // Object
  1000. },
  1001. setType: function(/* Object */o){
  1002. // summary:
  1003. // Post-process a parsed title to set a type on it.
  1004. if(o.guid.indexOf("discs")>-1){
  1005. o.type = "disc";
  1006. }
  1007. else if (o.guid.indexOf("programs")>-1){
  1008. o.type = "episode";
  1009. }
  1010. else if (o.guid.indexOf("series")>-1){
  1011. if(o.guid.indexOf("seasons")>-1){
  1012. o.type = "season";
  1013. } else {
  1014. o.type = "series";
  1015. }
  1016. }
  1017. else {
  1018. o.type = "movie"; // generic
  1019. }
  1020. }
  1021. };
  1022. p.queues = {
  1023. // summary:
  1024. // The XML parser for queue information (discs, instant, saved, rental history)
  1025. fromXml: function(/* XmlNode */node, /* Object? */obj){
  1026. // summary:
  1027. // Parse the returned XML into an object to be used by the application.
  1028. // object representing a queue item. Note that the title info is
  1029. // deliberately limited.
  1030. var item = {
  1031. queue: "/queues/disc",
  1032. guid: null,
  1033. id: null,
  1034. position: null,
  1035. availability: null,
  1036. updated: null,
  1037. shipped: null,
  1038. watched: null,
  1039. estimatedArrival: null,
  1040. returned: null,
  1041. viewed: null,
  1042. format: null,
  1043. title: {
  1044. guid: null,
  1045. title: null
  1046. }
  1047. };
  1048. var info = node.ownerDocument.evaluate("./*", node), currentNode;
  1049. while(currentNode = info.iterateNext()){
  1050. switch(currentNode.tagName){
  1051. case "id":
  1052. item.guid = dojo.trim(currentNode.textContent);
  1053. item.id = item.guid;
  1054. var l = item.guid.split("/");
  1055. l.pop(); // pull the id off
  1056. item.queue = l.slice(5).join("/");
  1057. break;
  1058. case "position":
  1059. item.position = parseInt(currentNode.textContent, 10);
  1060. break;
  1061. case "category":
  1062. var scheme = currentNode.getAttribute("scheme");
  1063. if(scheme == "http://api.netflix.com/categories/queue_availability"){
  1064. item.availability = dojo.trim(currentNode.textContent);
  1065. }
  1066. else if(scheme == "http://api.netflix.com/categories/title_formats"){
  1067. item.format = currentNode.getAttribute("term");
  1068. }
  1069. break;
  1070. case "updated":
  1071. item.updated = parseDate(currentNode.textContent);
  1072. break;
  1073. case "shipped_date":
  1074. item.shipped = parseDate(currentNode.textContent);
  1075. break;
  1076. case "watched_date":
  1077. item.watched = parseDate(currentNode.textContent);
  1078. break;
  1079. case "estimated_arrival_date":
  1080. item.estimatedArrival = parseDate(currentNode.textContent);
  1081. break;
  1082. case "returned_date":
  1083. item.returned = parseDate(currentNode.textContent);
  1084. case "viewed_time":
  1085. item.viewed = currentNode.textContent;
  1086. break;
  1087. case "link":
  1088. // we only care about the title this represents.
  1089. var rel = currentNode.getAttribute("rel");
  1090. if(rel == "http://schemas.netflix.com/catalog/title"){
  1091. // use the title parser on the main node here for basic info.
  1092. // Note that it is up to the calling code to merge this title's
  1093. // info with any existing info.
  1094. item.title = p.titles.fromXml(node);
  1095. }
  1096. else if(rel == "http://schemas.netflix.com/queues.available"){
  1097. // we do this here because for the available queues, Netflix embeds
  1098. // the position in the guid.
  1099. var l = currentNode.getAttribute("href");
  1100. // redo the id
  1101. item.id = l + "/" + item.guid.substr(item.guid.lastIndexOf("/")+1);
  1102. }
  1103. break;
  1104. }
  1105. }
  1106. if(obj){
  1107. item = util.mixin(obj, item);
  1108. }
  1109. return item; // Object
  1110. }
  1111. };
  1112. p.users = {
  1113. // summary:
  1114. // The XML parser for any user information
  1115. fromXml: function(/* XmlNode */node, /* Object? */obj){
  1116. // summary:
  1117. // Return a user object from the passed xml node.
  1118. var user = {
  1119. name: { first: null, last: null },
  1120. userId: null,
  1121. canInstantWatch: false,
  1122. preferredFormats: []
  1123. };
  1124. // ignore the links included for now.
  1125. var info = node.ownerDocument.evaluate("./*[name()!='link']", node), currentNode;
  1126. while(currentNode = info.iterateNext()){
  1127. switch(currentNode.tagName){
  1128. case "user_id":
  1129. user.userId = dojo.trim(currentNode.textContent);
  1130. break;
  1131. case "first_name":
  1132. user.name.first = dojo.trim(currentNode.textContent);
  1133. break;
  1134. case "last_name":
  1135. user.name.last = dojo.trim(currentNode.textContent);
  1136. break;
  1137. case "can_instant_watch":
  1138. user.canInstantWatch = dojo.trim(currentNode.textContent)=="true";
  1139. break;
  1140. case "preferred_formats":
  1141. dojo.query("category", currentNode).forEach(function(item){
  1142. user.preferredFormats.push(item.getAttribute("term"));
  1143. });
  1144. break;
  1145. }
  1146. }
  1147. if(obj){
  1148. obj = util.mixin(obj, user);
  1149. }
  1150. return user; // Object
  1151. }
  1152. };
  1153. p.people = {
  1154. // summary:
  1155. // The XML parser for any information on a person (actors, directors, etc.)
  1156. fromXml: function(/* XmlNode */node, /* Object? */obj){
  1157. // summary:
  1158. // Parse the information out of the passed XmlNode for people.
  1159. var person = {
  1160. id: null,
  1161. name: null,
  1162. bio: null,
  1163. filmography: null
  1164. };
  1165. var info = node.ownerDocument.evaluate("./name()", node), currentNode;
  1166. while(currentNode = info.iterateNext()){
  1167. switch(currentNode.tagName){
  1168. case "id":
  1169. case "name":
  1170. case "bio":
  1171. person[currentNode.tagName] = util.clean(dojo.trim(currentNode.textContent));
  1172. break;
  1173. case "link":
  1174. // ignore the alternate link
  1175. if(currentNode.getAttribute("rel") == "http://schemas.netflix.com/catalog/titles.filmography"){
  1176. person.filmography = currentNode.getAttribute("href");
  1177. }
  1178. break;
  1179. }
  1180. }
  1181. if(obj){
  1182. person = util.mixin(obj, person);
  1183. }
  1184. return person; // Object
  1185. }
  1186. };
  1187. p.status = {
  1188. // summary:
  1189. // The XML parser for any status-based updates (modifying a queue, ratings, etc.)
  1190. fromXml: function(/* XmlNode */node){
  1191. // summary:
  1192. // Parse the status info out of the passed node.
  1193. var obj = {};
  1194. for(var i=0, l=node.childNodes.length; i<l; i++){
  1195. var item = node.childNodes[i];
  1196. if(item.nodeType == 1){
  1197. switch(item.tagName){
  1198. case "status_code":
  1199. obj.code = dojo.trim(item.textContent);
  1200. break;
  1201. case "sub_code":
  1202. obj.subcode = dojo.trim(item.textContent);
  1203. break;
  1204. case "message":
  1205. case "etag":
  1206. obj[item.tagName] = dojo.trim(item.textContent);
  1207. break;
  1208. case "resources_created":
  1209. obj.created = dojo.query("queue_item", item).map(function(n){
  1210. return p.queues.fromXml(n);
  1211. });
  1212. break;
  1213. case "failed_title_refs":
  1214. obj.failed = dojo.query("link", item).map(function(n){
  1215. return {
  1216. guid: n.getAttribute("href"),
  1217. title: n.getAttribute("title")
  1218. };
  1219. });
  1220. break;
  1221. case "already_in_queue":
  1222. obj.inQueue = dojo.query("link", item).map(function(n){
  1223. return {
  1224. guid: n.getAttribute("href"),
  1225. title: n.getAttribute("title")
  1226. };
  1227. });
  1228. break;
  1229. }
  1230. }
  1231. }
  1232. return obj; // Object
  1233. }
  1234. };
  1235. })();
  1236. }
  1237. if(!dojo._hasResource["qd.services.online.feeds"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  1238. dojo._hasResource["qd.services.online.feeds"] = true;
  1239. dojo.provide("qd.services.online.feeds");
  1240. (function(){
  1241. var util = qd.services.util,
  1242. ps = qd.services.parser,
  1243. db = qd.services.data;
  1244. var rssFeeds = {
  1245. top25: [],
  1246. top100: null,
  1247. newReleases: null
  1248. };
  1249. var top25Feeds = [], top100Feed, newReleasesFeed;
  1250. var feedlistInit = function(){
  1251. db.fetch({
  1252. sql: "SELECT id, term, lastUpdated, isInstant, feed, xml FROM GenreFeed ORDER BY term",
  1253. result: function(data, result){
  1254. dojo.forEach(data, function(item){
  1255. if(item.feed.indexOf("Top100RSS")>-1){
  1256. rssFeeds.top100 = item;
  1257. } else if(item.feed.indexOf("NewReleasesRSS")>-1){
  1258. rssFeeds.newReleases = item;
  1259. } else {
  1260. rssFeeds.top25.push(item);
  1261. }
  1262. });
  1263. }
  1264. });
  1265. };
  1266. if(db.initialized){
  1267. feedlistInit();
  1268. } else {
  1269. var h = dojo.connect(db, "onInitialize", function(){
  1270. dojo.disconnect(h);
  1271. feedlistInit();
  1272. });
  1273. }
  1274. function getFeedObject(url){
  1275. if(url == rssFeeds.top100.feed){ return rssFeeds.top100; }
  1276. if(url == rssFeeds.newReleases.feed){ return rssFeeds.newReleases; }
  1277. for(var i=0; i<rssFeeds.top25.length; i++){
  1278. if(url == rssFeeds.top25[i].feed){
  1279. return rssFeeds.top25[i];
  1280. }
  1281. }
  1282. return null;
  1283. }
  1284. dojo.mixin(qd.services.online.feeds, {
  1285. // summary:
  1286. // The online-based service for handling Netflix's public RSS feeds.
  1287. list: function(){
  1288. // summary:
  1289. // Return the list of Top 25 RSS feeds.
  1290. return rssFeeds.top25; // Object[]
  1291. },
  1292. top100: function(){
  1293. // summary:
  1294. // Return the top 100 feed object.
  1295. return rssFeeds.top100; // Object
  1296. },
  1297. newReleases: function(){
  1298. // summary:
  1299. // Return the New Releases feed object.
  1300. return rssFeeds.newReleases; // Object
  1301. },
  1302. /*=====
  1303. qd.services.online.feeds.fetch.__FetchArgs = function(url, result, error){
  1304. // summary:
  1305. // Keyword object for getting the contents of an RSS feed.
  1306. // url: String
  1307. // The URL of the feed to fetch.
  1308. // result: Function?
  1309. // The callback to be fired when the RSS feed has been fetched and parsed.
  1310. // error: Function?
  1311. // The errback function to be fired if there is an error during the course of the fetch.
  1312. }
  1313. =====*/
  1314. fetch: function(/* qd.services.online.feeds.fetch.__FetchArgs */kwArgs){
  1315. // summary:
  1316. // Fetch the feed at the url in the feed object, and
  1317. // store/cache the feed when returned.
  1318. var dfd = util.prepare(kwArgs), feed=getFeedObject(kwArgs.url);
  1319. dojo.xhrGet({
  1320. url: kwArgs.url,
  1321. handleAs: "xml",
  1322. load: function(xml, ioArgs){
  1323. var node, parsed = [], items = xml.evaluate("//channel/item", xml);
  1324. while(node = items.iterateNext()){
  1325. parsed.push(ps.titles.fromRss(node));
  1326. }
  1327. // pre and post-process the results (image caching)
  1328. qd.services.online.process(parsed, dfd);
  1329. // push the xml doc into the database
  1330. var sql = "UPDATE GenreFeed SET LastUpdated = DATETIME(), xml=:xml WHERE id=:id ";
  1331. db.execute({
  1332. sql: sql,
  1333. params:{
  1334. id: feed.id,
  1335. xml: new XMLSerializer().serializeToString(xml).replace(/'/g, "''")
  1336. },
  1337. result: function(data){
  1338. // console.log("Stored the feed ("+feed.term+")");
  1339. }
  1340. });
  1341. },
  1342. error: function(err, ioArgs){
  1343. dfd.errback(new Error("qd.service.feeds.fetch: an error occurred when trying to get " + kwArgs.url));
  1344. }
  1345. });
  1346. return dfd; // dojo.Deferred
  1347. }
  1348. });
  1349. })();
  1350. }
  1351. if(!dojo._hasResource["qd.services.online.titles"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  1352. dojo._hasResource["qd.services.online.titles"] = true;
  1353. dojo.provide("qd.services.online.titles");
  1354. (function(){
  1355. var ps = qd.services.parser,
  1356. db = qd.services.data,
  1357. util = qd.services.util;
  1358. var some = encodeURIComponent("discs,episodes,seasons,synopsis,formats, screen formats"),
  1359. expand = encodeURIComponent("awards,cast,directors,discs,episodes,formats,languages and audio,screen formats,seasons,synopsis"); // similars
  1360. function saveTitle(item){
  1361. var param = {
  1362. guid: item.guid,
  1363. link: item.guid,
  1364. title: item.title,
  1365. synopsis: item.synopsis,
  1366. rating: item.rating,
  1367. item: dojo.toJson(item)
  1368. };
  1369. db.execute({
  1370. sql: "SELECT json FROM Title WHERE guid=:guid",
  1371. params: {
  1372. guid: item.guid
  1373. },
  1374. result: function(data){
  1375. if(data && data.length){
  1376. // mix-in the passed json with the stored one.
  1377. item = util.mixin(dojo.fromJson(data[0].json), item);
  1378. param.item = dojo.toJson(item);
  1379. }
  1380. db.execute({
  1381. sql: "REPLACE INTO Title (guid, link, title, lastUpdated, synopsis, rating, json)"
  1382. + " VALUES(:guid, :link, :title, DATETIME(), :synopsis, :rating, :item)",
  1383. params: param,
  1384. result: function(data){
  1385. // don't need this, keeping it for logging purposes.
  1386. },
  1387. error: function(err){
  1388. console.warn("titles.save: ERROR!", error);
  1389. }
  1390. });
  1391. }
  1392. });
  1393. }
  1394. function fetchItem(url, dfd, term){
  1395. var signer = qd.app.authorization;
  1396. dojo.xhrGet(dojox.io.OAuth.sign("GET", {
  1397. url: url,
  1398. handleAs: "xml",
  1399. load: function(xml, ioArgs){
  1400. // get the right node, parse it, cache it, and callback it.
  1401. var node, items = xml.evaluate((term!==undefined?"//catalog_title":"//ratings/ratings_item"), xml), ob;
  1402. while(node = items.iterateNext()){
  1403. if(term !== undefined){
  1404. var test = node.getElementsByTagName("title");
  1405. if(test.length && test[0].getAttribute("regular") == util.clean(term)){
  1406. ob = ps.titles.fromXml(node);
  1407. break;
  1408. }
  1409. } else {
  1410. ob = ps.titles.fromXml(node);
  1411. break;
  1412. }
  1413. }
  1414. if(ob){
  1415. if(term !== undefined){
  1416. // hit it again, this time with a guid (so we can get some ratings)
  1417. url = "http://api.netflix.com/users/" + signer.userId + "/ratings/title"
  1418. + "?title_refs=" + ob.guid
  1419. + "&expand=" + expand;
  1420. fetchItem(url, dfd);
  1421. } else {
  1422. // fire off the callback, passing it the returned object.
  1423. qd.services.item(ob);
  1424. qd.services.online.process(ob, dfd);
  1425. saveTitle(ob);
  1426. }
  1427. } else {
  1428. // in case we don't get an exact term match back from Netflix.
  1429. var e = new Error("No term match from Netflix for " + term);
  1430. e.xml = xml;
  1431. dfd.errback(e, ioArgs);
  1432. }
  1433. },
  1434. error: function(err, ioArgs){
  1435. // TODO: modify for an invalid signature check.
  1436. var e = new Error(err);
  1437. e.xml = ioArgs.xhr.responseXML;
  1438. dfd.errback(e, ioArgs);
  1439. }
  1440. }, signer), false);
  1441. }
  1442. function batchRatingsFetch(titles, args, ratings){
  1443. // summary:
  1444. // Private function to do actual ratings calls.
  1445. var signer = qd.app.authorization,
  1446. refs= [];
  1447. for(var p in titles){
  1448. refs.push(p);
  1449. }
  1450. var url = "http://api.netflix.com/users/" + signer.userId + "/ratings/title"
  1451. + "?title_refs=" + refs.join(",");
  1452. var signedArgs = dojox.io.OAuth.sign("GET", {
  1453. url: url,
  1454. handleAs: "xml",
  1455. load: function(xml, ioArgs){
  1456. // parse the info, merge and save
  1457. var node, items = xml.evaluate("//ratings/ratings_item", xml), a = [], toSave = [];
  1458. while(node = items.iterateNext()){
  1459. var tmp = ps.titles.fromXml(node),
  1460. r = tmp.ratings,
  1461. title = titles[tmp.guid];
  1462. if(!title || dojo.isString(title)){
  1463. // for some reason we only have a string.
  1464. title = tmp;
  1465. } else {
  1466. if(r.predicted !== null){ title.ratings.predicted = r.predicted; }
  1467. if(r.user !== null){ title.ratings.user = r.user; }
  1468. }
  1469. a.push(title);
  1470. qd.services.item(title);
  1471. toSave.push(title);
  1472. ratings.push(title);
  1473. }
  1474. args.result.call(args, a, ioArgs);
  1475. dojo.forEach(toSave, function(item){
  1476. saveTitle(item);
  1477. });
  1478. },
  1479. error: function(err, ioArgs){
  1480. console.warn("Batch ratings fetch: ", err);
  1481. }
  1482. }, signer);
  1483. return dojo.xhrGet(signedArgs);
  1484. }
  1485. function encode(s){
  1486. if(!s){ return ""; }
  1487. return encodeURIComponent(s)
  1488. .replace(/\!/g, "%2521")
  1489. .replace(/\*/g, "%252A")
  1490. .replace(/\'/g, "%2527")
  1491. .replace(/\(/g, "%2528")
  1492. .replace(/\)/g, "%2529");
  1493. }
  1494. dojo.mixin(qd.services.online.titles, {
  1495. // summary:
  1496. // The online-based service to get any title information, including
  1497. // ratings and recommendations.
  1498. save: function(/* Object */item){
  1499. // summary:
  1500. // Save the passed title to the database.
  1501. saveTitle(item);
  1502. },
  1503. clear: function(){
  1504. // summary:
  1505. // Clear all titles out of the database.
  1506. db.execute({
  1507. sql: "DELETE FROM Title",
  1508. result: function(data){
  1509. // don't do anything for now.
  1510. }
  1511. });
  1512. },
  1513. /*=====
  1514. qd.services.online.titles.find.__FindArgs = function(term, start, max, result, error){
  1515. // summary:
  1516. // Arguments object for doing a title search
  1517. // term: String
  1518. // The partial title to be looking for.
  1519. // start: Number?
  1520. // The page index to start on. Default is 0.
  1521. // max: Number?
  1522. // The maximum number of results to find. Default is 25 (supplied by Netflix).
  1523. // result: Function?
  1524. // The callback function that will be executed when a result is
  1525. // fetched.
  1526. // error: Function?
  1527. // The callback function to be executed if there is an error in fetching.
  1528. }
  1529. =====*/
  1530. find: function(/* qd.services.online.titles.find.__FindArgs */kwArgs){
  1531. // summary:
  1532. // Use the Netflix API directly, and try to cache any results as they come.
  1533. var dfd = util.prepare(kwArgs),
  1534. signer = qd.app.authorization;
  1535. var t = encodeURIComponent(util.clean(kwArgs.term));
  1536. if(t.match(/!|\*|\'|\(|\)/g)){
  1537. t = encode(t);
  1538. }
  1539. dojo.xhrGet(dojox.io.OAuth.sign("GET", {
  1540. url: "http://api.netflix.com/catalog/titles?"
  1541. + "term=" + t
  1542. + (kwArgs.start ? "&start_index=" + kwArgs.start : "")
  1543. + (kwArgs.max ? "&max_results=" + kwArgs.max : "")
  1544. + "&expand=" + some,
  1545. handleAs: "xml",
  1546. load: function(response, ioArgs){
  1547. // assemble the results and return as an object.
  1548. var n = response.evaluate("/catalog_titles/number_of_results", response).iterateNext(),
  1549. o = {
  1550. number_found: n ? parseInt(dojo.trim(n.textContent), 10) : 0,
  1551. search_term: kwArgs.term,
  1552. results: [],
  1553. sort_by: "Relevance"
  1554. };
  1555. // parse the movie results.
  1556. var items = response.evaluate("/catalog_titles/catalog_title", response), node, sqls=[];
  1557. while(node = items.iterateNext()){
  1558. var item = ps.titles.fromXml(node);
  1559. item.art.large = util.image.url(item.art.large);
  1560. o.results.push(item);
  1561. qd.services.item(item);
  1562. sqls.push(item);
  1563. }
  1564. // fire the callback before these titles are pushed into the database.
  1565. dfd.callback(o, ioArgs);
  1566. // go and execute the saved SQL in the background
  1567. dojo.forEach(sqls, function(item){
  1568. saveTitle(item);
  1569. if(item.art.large && item.art.large.indexOf("http://")>-1){
  1570. util.image.store(item.art.large).addCallback(function(u){
  1571. item.art.large = u;
  1572. });
  1573. }
  1574. });
  1575. },
  1576. error: function(err, ioArgs){
  1577. // TODO: modify for an invalid signature check.
  1578. var e = new Error(err);
  1579. e.xml = ioArgs.xhr.responseXML;
  1580. dfd.errback(e, ioArgs);
  1581. }
  1582. }, signer));
  1583. return dfd; // dojo.Deferred
  1584. },
  1585. /*=====
  1586. qd.services.online.titles.fetch.__FetchArgs = function(term, title, result, error){
  1587. // summary:
  1588. // Arguments object for fetching movie details
  1589. // term: String?
  1590. // The full title of the Netflix title in question, as provided by the
  1591. // Netflix RSS feeds.
  1592. // guid: String?
  1593. // The guid of the title in question as passed back by the Netflix servers.
  1594. // result: Function?
  1595. // The callback function that will be executed when a result is
  1596. // fetched.
  1597. // error: Function?
  1598. // The callback function to be executed if there is an error in fetching.
  1599. }
  1600. =====*/
  1601. fetch: function(/* qd.services.online.titles.fetch.__FetchArgs */kwArgs){
  1602. // summary:
  1603. // Retrieve title details from the Netflix API.
  1604. if(!kwArgs.guid && !kwArgs.term){
  1605. throw new Error("qd.services.online.titles.fetch: you must pass either a guid or a term.");
  1606. }
  1607. var dfd = util.prepare(kwArgs);
  1608. // look to see if the object is in memory first.
  1609. var test;
  1610. if(kwArgs.term){
  1611. test = qd.services.itemByTerm(kwArgs.term);
  1612. } else {
  1613. test = qd.services.item(kwArgs.guid);
  1614. }
  1615. if(test && test.ratings.user !== null && test.synopsis){
  1616. setTimeout(function(){
  1617. dfd.callback(test);
  1618. }, 10);
  1619. return dfd;
  1620. }
  1621. // Check the cache second.
  1622. var sql = "SELECT * FROM Title WHERE guid=:guid AND json IS NOT NULL";
  1623. var params = { guid: kwArgs.guid };
  1624. if(kwArgs.term){
  1625. sql = "SELECT * FROM Title WHERE title=:term AND json IS NOT NULL";
  1626. params = { term: kwArgs.term };
  1627. }
  1628. var url;
  1629. if(kwArgs.term){
  1630. var t = encodeURIComponent(util.clean(kwArgs.term));
  1631. if(t.match(/!|\*|\'|\(|\)/g)){
  1632. t = encode(t);
  1633. }
  1634. url = "http://api.netflix.com/catalog/titles?"
  1635. + "term=" + t
  1636. + "&expand=" + some
  1637. + "&max_results=24"; // hardcoded, we want this to be as fast as possible
  1638. } else {
  1639. url = "http://api.netflix.com/users/" + qd.app.authorization.userId + "/ratings/title"
  1640. + "?title_refs=" + kwArgs.guid
  1641. + "&expand=" + expand;
  1642. }
  1643. db.execute({
  1644. sql: sql,
  1645. params: params,
  1646. result: function(data, result){
  1647. if(data && data.length){
  1648. var title = dojo.fromJson(data[0].json);
  1649. if(title.ratings.user !== null && title.synopsis){
  1650. qd.services.item(title);
  1651. setTimeout(function(){
  1652. dfd.callback(title, result);
  1653. }, 10);
  1654. } else {
  1655. // get it from Netflix
  1656. fetchItem(url, dfd, kwArgs.term);
  1657. }
  1658. } else {
  1659. // get it from Netflix
  1660. fetchItem(url, dfd, kwArgs.term);
  1661. }
  1662. }
  1663. });
  1664. return dfd; // dojo.Deferred
  1665. },
  1666. /*=====
  1667. qd.services.online.titles.autosuggest.__AutosuggestArgs = function(term, result, error){
  1668. // summary:
  1669. // Arguments object for fetching movie details
  1670. // term: String?
  1671. // The partial string that an autocomplete should match.
  1672. // result: Function?
  1673. // The callback function that will be executed when a result is
  1674. // fetched.
  1675. // error: Function?
  1676. // The callback function to be executed if there is an error in fetching.
  1677. }
  1678. =====*/
  1679. autosuggest: function(/* qd.services.online.titles.autosuggest.__AutosuggestArgs */kwArgs){
  1680. // summary:
  1681. // Get the autocomplete terms from Netflix, given the right term.
  1682. var dfd = util.prepare(kwArgs),
  1683. signer = qd.app.authorization;
  1684. dojo.xhrGet(dojox.io.OAuth.sign("GET", {
  1685. url: "http://api.netflix.com/catalog/titles/autocomplete"
  1686. + "?term=" + encode(kwArgs.term),
  1687. handleAs: "xml",
  1688. load: function(xml, ioArgs){
  1689. var a = [], node, items = xml.evaluate("//@short", xml);
  1690. while(node = items.iterateNext()){
  1691. a.push(node.nodeValue);
  1692. }
  1693. dfd.callback(a, ioArgs);
  1694. },
  1695. error: function(err, ioArgs){
  1696. dfd.errback(err, ioArgs);
  1697. }
  1698. }, signer));
  1699. return dfd; // dojo.Deferred
  1700. },
  1701. /*=====
  1702. qd.services.online.titles.rated.__RatedArgs = function(guids, result, error){
  1703. // summary:
  1704. // Named arguments used for fetching the contents of a queue.
  1705. // guids: Array
  1706. // The list of guids to get ratings for.
  1707. // result: Function?
  1708. // The callback function on successful retrieval.
  1709. // error: Function?
  1710. // The errback function on failure.
  1711. }
  1712. =====*/
  1713. rated: function(/* qd.services.online.titles.rated.__RatedArgs */kwArgs){
  1714. // summary:
  1715. // Fetch ratings info for all of the guids passed in the kwArgs.
  1716. // Try to do most of this as local as possible, by doing the following:
  1717. // 1. get all the titles from the database.
  1718. // 2. check which ones already have the predicted and actual ratings.
  1719. // 3. assemble a list of titles without that information.
  1720. // 4. fetch those titles from Netflix, and merge with the existing ones.
  1721. // 5. save the titles with new ratings info.
  1722. var dfd = new dojo.Deferred();
  1723. var ratings = [];
  1724. db.fetch({
  1725. sql: "SELECT json FROM Title WHERE guid IN ('" + (kwArgs.guids || []).join("','") + "') ORDER BY title",
  1726. result: function(data){
  1727. var good = [], missing = [], item, tested = {};
  1728. if(data){
  1729. // run through the data results and see what we really need to go get from Netflix.
  1730. for(var i=0; i<data.length; i++){
  1731. item = dojo.fromJson(data[i].json);
  1732. if(item && item.ratings && item.ratings.user !== null){
  1733. good.push(item);
  1734. } else {
  1735. missing.push(item);
  1736. }
  1737. if(item){
  1738. tested[item.guid] = true;
  1739. }
  1740. }
  1741. } else {
  1742. missing = kwArgs.guids.slice(0);
  1743. }
  1744. // now double check and make sure that any *not* tested guids are pushed into missing.
  1745. dojo.forEach(kwArgs.guids, function(guid){
  1746. if(!tested[guid]){
  1747. missing.push(guid);
  1748. }
  1749. });
  1750. if(good.length){
  1751. dojo.forEach(good, function(item){
  1752. qd.services.item(item);
  1753. });
  1754. kwArgs.result.call(kwArgs, good);
  1755. ratings = ratings.concat(good);
  1756. }
  1757. if(missing.length){
  1758. // we have at least one missing title, so let's go get that info from Netflix.
  1759. var chunks = [], limit = 24, position = 0;
  1760. while(position < missing.length){
  1761. var n = Math.min(limit, missing.length - position);
  1762. chunks.push(missing.slice(position, position + n));
  1763. position += n;
  1764. }
  1765. // start the missing fetches.
  1766. var pos = 0, timer;
  1767. timer = setInterval(function(){
  1768. var chunk = chunks[pos++];
  1769. if(chunk){
  1770. // pre-process and call the internal function.
  1771. var o = {};
  1772. dojo.forEach(chunk, function(item){
  1773. if(dojo.isString(item)){
  1774. o[item] = item;
  1775. } else {
  1776. o[item.guid] = item;
  1777. }
  1778. });
  1779. batchRatingsFetch(o, kwArgs, ratings);
  1780. } else {
  1781. clearInterval(timer);
  1782. dfd.callback(ratings);
  1783. }
  1784. }, 250);
  1785. } else {
  1786. dfd.callback(ratings);
  1787. }
  1788. }
  1789. });
  1790. return dfd; // dojo.Deferred
  1791. },
  1792. /*=====
  1793. qd.services.online.titles.rate.__RateArgs = function(guid, rating, result, error){
  1794. // summary:
  1795. // Named arguments object for handling rating a movie.
  1796. // guid: String
  1797. // The id of the title to be updated.
  1798. // rating: String
  1799. // The new rating for the movie
  1800. // result: Function?
  1801. // The callback fired when the rating is completed.
  1802. // error: Function?
  1803. // The errback fired in case of an error.
  1804. }
  1805. =====*/
  1806. rate: function(/* qd.services.online.titles.rate.__RateArgs */kwArgs){
  1807. // summary:
  1808. // Rate the title as referenced by kwArgs.guid.
  1809. var dfd = util.prepare(kwArgs),
  1810. signer = qd.app.authorization;
  1811. var item = qd.services.item(kwArgs.guid);
  1812. if(!item){
  1813. setTimeout(function(){
  1814. dfd.errback(new Error("qd.service.rate: cannot rate an item with a guid of " + kwArgs.guid));
  1815. }, 10);
  1816. return dfd; // dojo.Deferred
  1817. }
  1818. // check to see if this is an update
  1819. var url = "http://api.netflix.com/users/" + signer.userId + "/ratings/title/actual";
  1820. if(item.ratings.user){
  1821. // this is an update, make sure there's a rating id.
  1822. var args = {
  1823. url: url + "/" + item.guid.substr(item.guid.lastIndexOf("/")+1),
  1824. content: {
  1825. rating: kwArgs.rating,
  1826. method: "PUT"
  1827. },
  1828. handleAs: "xml",
  1829. load: function(xml, ioArgs){
  1830. var s = ps.status.fromXml(xml.documentElement);
  1831. if(s.code == "200"){
  1832. if(kwArgs.rating == "not_interested" || kwArgs.rating == "no_opinion"){
  1833. item.ratings.user = null;
  1834. }else{
  1835. item.ratings.user = kwArgs.rating;
  1836. }
  1837. qd.services.item(item);
  1838. saveTitle(item);
  1839. dfd.callback(kwArgs.rating, ioArgs);
  1840. } else {
  1841. dfd.errback(s);
  1842. }
  1843. },
  1844. error: function(err, ioArgs){
  1845. dfd.errback(err, ioArgs);
  1846. }
  1847. };
  1848. args = dojox.io.OAuth.sign("GET", args, signer);
  1849. dojo.xhrGet(args);
  1850. } else {
  1851. // this is a new rating.
  1852. var args = {
  1853. url: url
  1854. + "?title_ref=" + encodeURIComponent(kwArgs.guid)
  1855. + "&rating=" + kwArgs.rating,
  1856. handleAs: "xml",
  1857. load: function(xml, ioArgs){
  1858. var s = ps.status.fromXml(xml.documentElement);
  1859. if(s.code == "201"){
  1860. if(kwArgs.rating == "not_interested" || kwArgs.rating == "no_opinion"){
  1861. item.ratings.user = null;
  1862. }else{
  1863. item.ratings.user = kwArgs.rating;
  1864. }
  1865. qd.services.item(item);
  1866. saveTitle(item);
  1867. dfd.callback(kwArgs.rating, ioArgs);
  1868. } else {
  1869. dfd.errback(s);
  1870. }
  1871. },
  1872. error: function(err, ioArgs){
  1873. dfd.errback(err, ioArgs);
  1874. }
  1875. };
  1876. args = dojox.io.OAuth.sign("POST", args, signer);
  1877. dojo.xhrPost(args);
  1878. }
  1879. return dfd; // dojo.Deferred
  1880. },
  1881. /*=====
  1882. qd.services.titles.online.recommendations.__RecArgs = function(start, max, result, error){
  1883. // summary:
  1884. // Keyword arguments to be fed to the recommendations method.
  1885. // start: Integer?
  1886. // The starting index to fetch. Defaults to the Netflix default (0).
  1887. // max: Integer?
  1888. // The max number of results to fetch. Defaults to the Netflix default (25).
  1889. // result: Function?
  1890. // The callback to be run when results are returned.
  1891. // error: Function?
  1892. // The callback to be run if an error occurs.
  1893. }
  1894. =====*/
  1895. recommendations: function(/* qd.services.titles.online.recommendations.__RecArgs */kwArgs){
  1896. // summary:
  1897. // Fetch user recommendations from Netflix.
  1898. var dfd = util.prepare(kwArgs),
  1899. signer = qd.app.authorization;
  1900. // when online, always get this from the servers.
  1901. var qs = [];
  1902. if(kwArgs.start){ qs.push("start_index=" + kwArgs.start); }
  1903. if(kwArgs.max){ qs.push("max_results=" + kwArgs.max); }
  1904. var url = "http://api.netflix.com/users/" + signer.userId + "/recommendations"
  1905. + "?expand=" + some
  1906. + (qs.length ? "&" + qs.join("&") : "");
  1907. dojo.xhrGet(dojox.io.OAuth.sign("GET", {
  1908. url: url,
  1909. handleAs: "xml",
  1910. load: function(xml, ioArgs){
  1911. var node,
  1912. items = xml.evaluate("recommendations/recommendation", xml),
  1913. results = [], sqls = [];
  1914. while(node = items.iterateNext()){
  1915. var item = ps.titles.fromXml(node);
  1916. results.push(item);
  1917. sqls.push(item);
  1918. saveTitle(item);
  1919. qd.services.item(item);
  1920. }
  1921. // do the callback
  1922. qd.services.online.process(results, dfd);
  1923. // push it into the database.
  1924. setTimeout(function(){
  1925. var sql = "REPLACE INTO Recommendation(guid, title, lastUpdated) "
  1926. + " VALUES (:guid, :title, DATETIME())";
  1927. dojo.forEach(sqls, function(item){
  1928. db.execute({
  1929. sql: sql,
  1930. params: {
  1931. guid: item.guid,
  1932. title: item.title
  1933. },
  1934. result: function(data){
  1935. //console.warn("Recommended title '" + item.title + "' saved.");
  1936. }
  1937. });
  1938. });
  1939. }, 500);
  1940. },
  1941. error: function(err, ioArgs){
  1942. dfd.errback(err, ioArgs);
  1943. }
  1944. }, signer));
  1945. return dfd; // dojo.Deferred
  1946. }
  1947. });
  1948. })();
  1949. }
  1950. if(!dojo._hasResource["qd.services.online.queues"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  1951. dojo._hasResource["qd.services.online.queues"] = true;
  1952. dojo.provide("qd.services.online.queues");
  1953. (function(){
  1954. var ps = qd.services.parser,
  1955. db = qd.services.data,
  1956. util = qd.services.util,
  1957. storage = qd.services.storage;
  1958. var tags = {
  1959. disc: null,
  1960. instant: null
  1961. };
  1962. function etag(queue, tag){
  1963. if(tag){
  1964. tags[queue] = tag;
  1965. }
  1966. return tags[queue];
  1967. }
  1968. dojo.mixin(qd.services.online.queues, {
  1969. // summary:
  1970. // The online-based service for queue information and manipulation.
  1971. paths: {
  1972. GET: {
  1973. "queues/disc/available" :"//queue/queue_item",
  1974. "queues/disc/saved" :"//queue/queue_item",
  1975. "at_home" :"//at_home/at_home_item",
  1976. "queues/instant/available" :"//queue/queue_item",
  1977. "rental_history/shipped" :"//rental_history/rental_history_item",
  1978. "rental_history/returned" :"//rental_history/rental_history_item",
  1979. "rental_history/watched" :"//rental_history/rental_history_item"
  1980. }
  1981. },
  1982. etag: function(/* String */queue, /* String? */tag){
  1983. // summary:
  1984. // Store or retreive the latest etag for the passed queue.
  1985. return etag(queue, tag); // String
  1986. },
  1987. clear: function(){
  1988. // summary:
  1989. // Clear both the queue cache and the transaction queue from the database.
  1990. db.execute({
  1991. sql: "DELETE FROM QueueCache",
  1992. result: function(data){
  1993. // don't do anything for now.
  1994. }
  1995. });
  1996. db.execute({
  1997. sql: "DELETE FROM TransactionQueue",
  1998. result: function(data){
  1999. // don't do anything for now.
  2000. }
  2001. });
  2002. },
  2003. /*=====
  2004. qd.services.online.queues.fetch.__FetchArgs = function(url, lastUpdated, start, max, result, error){
  2005. // summary:
  2006. // Named arguments used for fetching the contents of a queue.
  2007. // url: String
  2008. // The partial URL to be used (ex. queues/disc, at_home)
  2009. // lastUpdated: String?
  2010. // A UNIX timestamp, in seconds, with when you want to check it from. Useful for at home notifications.
  2011. // start: Number?
  2012. // The index at which to start the fetch. Defaults to 0.
  2013. // max: Number?
  2014. // The maximum number of items to fetch. Defaults to 500.
  2015. // result: Function?
  2016. // The callback function on successful retrieval.
  2017. // error: Function?
  2018. // The errback function on failure.
  2019. }
  2020. =====*/
  2021. fetch: function(/* qd.services.online.queues.fetch.__FetchArgs */kwArgs){
  2022. // summary:
  2023. // Fetch the queue based on the passed partial URL string.
  2024. var dfd = util.prepare(kwArgs),
  2025. signer = qd.app.authorization;
  2026. if(kwArgs.lastUpdated){
  2027. // convert from date to number.
  2028. kwArgs.lastUpdated = Math.floor(kwArgs.lastUpdated.valueOf()/1000);
  2029. }
  2030. // always get it from the server, but make sure you cache the queue on the user object
  2031. dojo.xhrGet(dojox.io.OAuth.sign("GET", {
  2032. url: "http://api.netflix.com/users/" + signer.userId + "/" + (kwArgs.url || "queues/disc/available")
  2033. + "?sort=queue_sequence"
  2034. + "&start_index=" + (kwArgs.start || 0)
  2035. + "&max_results=" + (kwArgs.max || 500)
  2036. + (kwArgs.lastUpdated ? "&updated_min=" + kwArgs.lastUpdated : "")
  2037. + "&expand=" + encodeURIComponent("discs,episodes,seasons,synopsis,formats"),
  2038. handleAs: "xml",
  2039. load: function(xml, ioArgs){
  2040. var results = [], node, items = xml.evaluate(qd.services.online.queues.paths.GET[(kwArgs.url || "queues/disc/available")], xml);
  2041. var test = xml.getElementsByTagName("etag"), tagq;
  2042. if(test && test.length){
  2043. if(kwArgs.url && kwArgs.url.indexOf("queues/disc")>-1){
  2044. tagq = "disc";
  2045. }
  2046. else if(kwArgs.url && kwArgs.url.indexOf("queues/instant")>-1){
  2047. tagq = "instant";
  2048. }
  2049. if(tagq){
  2050. etag(tagq, test[0].textContent);
  2051. }
  2052. }
  2053. while(node = items.iterateNext()){
  2054. var q = ps.queues.fromXml(node);
  2055. results.push(q);
  2056. qd.services.item(q);
  2057. qd.services.item(q.title);
  2058. }
  2059. // fire the callback before these titles are pushed into the database.
  2060. dfd.callback(results, ioArgs);
  2061. },
  2062. error: function(err, ioArgs){
  2063. dfd.errback(err, ioArgs);
  2064. }
  2065. }, signer), false);
  2066. return dfd; // dojo.Deferred
  2067. },
  2068. // specific things
  2069. atHome: function(/* qd.services.online.queues.fetch.__FetchArgs */kwArgs){
  2070. // summary:
  2071. // Fetch the user's At Home queue.
  2072. kwArgs = kwArgs || {};
  2073. kwArgs.url = "at_home";
  2074. var dfd = this.fetch(kwArgs);
  2075. dfd.addCallback(function(arr){
  2076. // cache the big and small images for this.
  2077. dojo.forEach(arr, function(item){
  2078. var art = item.title.art;
  2079. art.large = util.image.url(art.large);
  2080. if(art.large.indexOf("http://")>-1){
  2081. util.image.store(art.large).addCallback(function(){
  2082. art.large = util.image.url(art.large);
  2083. });
  2084. }
  2085. art.small = util.image.url(art.small);
  2086. if(art.small.indexOf("http://")>-1){
  2087. util.image.store(art.small).addCallback(function(){
  2088. art.small = util.image.url(art.small);
  2089. });
  2090. }
  2091. });
  2092. });
  2093. return dfd; // dojo.Deferred
  2094. },
  2095. discs: function(/* qd.services.online.queues.fetch.__FetchArgs */kwArgs){
  2096. // summary:
  2097. // Fetch the user's disc queue.
  2098. kwArgs = kwArgs || {};
  2099. kwArgs.url = "queues/disc/available";
  2100. var dfd = this.fetch(kwArgs);
  2101. return dfd; // dojo.Deferred
  2102. },
  2103. saved: function(/* qd.services.online.queues.fetch.__FetchArgs */kwArgs){
  2104. // summary:
  2105. // Fetch the user's saved discs.
  2106. kwArgs = kwArgs || {};
  2107. kwArgs.url = "queues/disc/saved";
  2108. var dfd = this.fetch(kwArgs);
  2109. return dfd; // dojo.Deferred
  2110. },
  2111. instant: function(/* qd.services.online.queues.fetch.__FetchArgs */kwArgs){
  2112. // summary:
  2113. // Fetch the user's instant queue.
  2114. kwArgs = kwArgs || {};
  2115. kwArgs.url = "queues/instant/available";
  2116. var dfd = this.fetch(kwArgs);
  2117. return dfd; // dojo.Deferred
  2118. },
  2119. watched: function(/* qd.services.online.queues.fetch.__FetchArgs */kwArgs){
  2120. // summary:
  2121. // Fetch the user's instant watched queue.
  2122. kwArgs = kwArgs || {};
  2123. kwArgs.url = "rental_history/watched";
  2124. var dfd = this.fetch(kwArgs);
  2125. return dfd; // dojo.Deferred
  2126. },
  2127. shipped: function(/* qd.services.online.queues.fetch.__FetchArgs */kwArgs){
  2128. // summary:
  2129. // Fetch the user's shipped disc history.
  2130. kwArgs = kwArgs || {};
  2131. kwArgs.url = "rental_history/shipped";
  2132. var dfd = this.fetch(kwArgs);
  2133. return dfd; // dojo.Deferred
  2134. },
  2135. returned: function(/* qd.services.online.queues.fetch.__FetchArgs */kwArgs){
  2136. // summary:
  2137. // Fetch the user's returned disc history.
  2138. kwArgs = kwArgs || {};
  2139. kwArgs.url = "rental_history/returned";
  2140. var dfd = this.fetch(kwArgs);
  2141. return dfd; // dojo.Deferred
  2142. },
  2143. /*=====
  2144. qd.services.online.queues.modify.__ModifyArgs = function(url, guid, title, format, position, result, error){
  2145. // summary:
  2146. // Named arguments used for fetching the contents of a queue.
  2147. // url: String
  2148. // The partial url (see fetch) of the queue to be adding the item to.
  2149. // guid: String
  2150. // The full id of the queue item to be added or moved.
  2151. // title: String
  2152. // The title/name of the item being altered.
  2153. // format: String?
  2154. // The desired format of the item to be added. Defaults to user's preferences.
  2155. // position: Number?
  2156. // The desired position of the item in the queue. Do not pass if adding an item.
  2157. // result: Function?
  2158. // The callback function on successful retrieval.
  2159. // error: Function?
  2160. // The errback function on failure.
  2161. }
  2162. =====*/
  2163. modify: function(/* qd.services.online.queues.add.__ModifyArgs */kwArgs){
  2164. // summary:
  2165. // Add or move an item in a queue. Note that for the online
  2166. // version, we can ignore the passed title.
  2167. var dfd = util.prepare(kwArgs),
  2168. signer = qd.app.authorization;
  2169. var tagq = kwArgs.url.indexOf("instant")>-1 ? "instant" : "disc";
  2170. var content = {
  2171. title_ref: kwArgs.guid,
  2172. etag: etag(tagq)
  2173. };
  2174. if(kwArgs.format){ content.format = kwArgs.format; }
  2175. if(kwArgs.position){ content.position = kwArgs.position; }
  2176. // build the query string and append it.
  2177. var qs = [];
  2178. for(var p in content){
  2179. qs.push(p + "=" + encodeURIComponent(content[p]));
  2180. }
  2181. var args = dojox.io.OAuth.sign("POST", {
  2182. url: "http://api.netflix.com/users/" + signer.userId + "/" + (kwArgs.url || "queues/disc")
  2183. + "?" + qs.join("&"),
  2184. handleAs: "xml",
  2185. load: function(xml, ioArgs){
  2186. var o = ps.status.fromXml(xml.documentElement);
  2187. if(o.etag){ etag(tagq, o.etag); }
  2188. dfd.callback(o, ioArgs);
  2189. },
  2190. error: function(err, ioArgs){
  2191. console.warn("qd.service.queues.modify: an error occurred.", err.status.results.message);
  2192. dfd.errback(err, ioArgs);
  2193. }
  2194. }, signer);
  2195. dojo.xhrPost(args);
  2196. return dfd; // dojo.Deferred
  2197. },
  2198. /*=====
  2199. qd.services.online.queues.remove.__RemoveArgs = function(url, guid, title, result, error){
  2200. // summary:
  2201. // Named arguments used for fetching the contents of a queue.
  2202. // url: String
  2203. // The partial URL of the queue to be modified, i.e. queues/disc/available.
  2204. // guid: String
  2205. // The partial id (after the userId) of the queue item to be removed.
  2206. // title: String
  2207. // The title/name of the item being removed.
  2208. // result: Function?
  2209. // The callback function on successful retrieval.
  2210. // error: Function?
  2211. // The errback function on failure.
  2212. }
  2213. =====*/
  2214. remove: function(/* qd.services.online.queues.remove.__RemoveArgs */kwArgs){
  2215. // summary:
  2216. // Remove an item from the queue. We can ignore the title here,
  2217. // since we are online.
  2218. // API NOTE:
  2219. // Remove by ID not GUID. EX: 404309
  2220. var dfd = util.prepare(kwArgs),
  2221. signer = qd.app.authorization;
  2222. var tagq = kwArgs.url.indexOf("instant")>-1 ? "instant":"disc";
  2223. dojo.xhrDelete(dojox.io.OAuth.sign("DELETE", {
  2224. url: "http://api.netflix.com/users/" + signer.userId + "/" + kwArgs.url + "/" + kwArgs.guid + "?etag=" + etag(tagq),
  2225. handleAs: "xml",
  2226. load: function(xml, ioArgs){
  2227. var o = ps.status.fromXml(xml.documentElement);
  2228. if(o.etag){ etag(tagq, o.etag); }
  2229. dfd.callback(o, ioArgs);
  2230. // go fetch a new etag
  2231. qd.service.queues[tagq=="instant"?"instant":"discs"]({ max: 1 });
  2232. },
  2233. error: function(err, ioArgs){
  2234. dfd.errback(err, ioArgs);
  2235. }
  2236. }, signer));
  2237. return dfd; // dojo.Deferred
  2238. },
  2239. cache: function(/* String */queue, /* Array */list){
  2240. // summary:
  2241. // Cache the current state of the queue.
  2242. // if(queue == "history") { return; }
  2243. var map = dojo.map(list, function(listItem){
  2244. // strip out the useless stuff.
  2245. var item = dojo.clone(listItem.item);
  2246. if("detailsStr" in item){ delete item.detailsStr; }
  2247. if("genreStr" in item){ delete item.genreStr; }
  2248. if("instantStr" in item){ delete item.instantStr; }
  2249. if("returnedStr" in item){ delete item.returnedStr; }
  2250. if("starRatingEnabledStr" in item){ delete item.starRatingEnabledStr; }
  2251. return item;
  2252. });
  2253. setTimeout(function(){
  2254. db.execute({
  2255. sql: "REPLACE INTO QueueCache (queue, json, lastUpdated) VALUES (:queue, :json, DATETIME())",
  2256. params: {
  2257. queue: queue,
  2258. json: dojo.toJson(map)
  2259. }
  2260. });
  2261. }, 500);
  2262. },
  2263. // stubs to prevent any kind of Function Not Defined errors.
  2264. addMovieById: function(){ return; },
  2265. addMovieByTerm: function(){ return; }
  2266. });
  2267. })();
  2268. }
  2269. if(!dojo._hasResource["qd.services.online.user"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  2270. dojo._hasResource["qd.services.online.user"] = true;
  2271. dojo.provide("qd.services.online.user");
  2272. (function(){
  2273. var ps = qd.services.parser;
  2274. dojo.mixin(qd.services.online.user, {
  2275. // summary:
  2276. // The online-based service to fetch user information (user name, prefs, etc.)
  2277. /*=====
  2278. qd.services.onilne.user.fetch.__FetchArgs = function(result, error){
  2279. // summary:
  2280. // The keyword arguments object passed to fetch.
  2281. // result: Function?
  2282. // The callback to be fired on success.
  2283. // error: Function?
  2284. // The errback to be fired in case of an error.
  2285. }
  2286. =====*/
  2287. fetch: function(/* qd.services.online.user.fetch.__FetchArgs */kwArgs){
  2288. // summary:
  2289. // Fetch the current user's information from the Netflix servers.
  2290. var dfd = new dojo.Deferred(),
  2291. signer = qd.app.authorization;
  2292. dojo.xhrGet(dojox.io.OAuth.sign("GET", {
  2293. url: "http://api.netflix.com/users/" + signer.userId,
  2294. handleAs: "xml",
  2295. load: function(xml, ioArgs){
  2296. var o = ps.users.fromXml(xml.documentElement);
  2297. dfd.callback(o, ioArgs);
  2298. },
  2299. error: function(err, ioArgs){
  2300. console.error("qd.services.online.user.fetch: ", err);
  2301. dfd.errback(err);
  2302. }
  2303. }, signer), false);
  2304. return dfd; // dojo.Deferred
  2305. }
  2306. });
  2307. })();
  2308. }
  2309. if(!dojo._hasResource["qd.services.online"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  2310. dojo._hasResource["qd.services.online"] = true;
  2311. dojo.provide("qd.services.online");
  2312. (function(){
  2313. var db = qd.services.data,
  2314. network = qd.services.network;
  2315. //////////////////////////////////////////////////////////////////////////////////////////////
  2316. // Synchronization functions
  2317. //////////////////////////////////////////////////////////////////////////////////////////////
  2318. var actions = [];
  2319. // wrapper functions around sync functionality...this is due to the application sandbox.
  2320. var methods = {
  2321. rate: function(args){
  2322. return qd.service.titles.rate(args);
  2323. },
  2324. add: function(args){
  2325. var dfd = new dojo.Deferred();
  2326. qd.app.queue.addMovieById(args.movieId, null, args.queue);
  2327. setTimeout(function(){
  2328. dfd.callback();
  2329. }, 250);
  2330. return dfd;
  2331. },
  2332. termAdd: function(args){
  2333. var dfd = new dojo.Deferred();
  2334. var queue = args.queue;
  2335. args.result = function(item){
  2336. qd.app.queue.addMovieById(item.guid, null, queue);
  2337. dfd.callback();
  2338. };
  2339. delete args.queue;
  2340. qd.service.titles.fetch(args);
  2341. return dfd;
  2342. },
  2343. modify: function(args){
  2344. return qd.service.queues.modify(args);
  2345. },
  2346. remove: function(args){
  2347. return qd.service.queues.remove(args);
  2348. },
  2349. discs: function(){
  2350. return qd.service.queues.discs({ max: 1 });
  2351. },
  2352. instant: function(){
  2353. return qd.service.queues.instant({ max: 1 });
  2354. }
  2355. };
  2356. function getActions(){
  2357. actions = [];
  2358. db.execute({
  2359. sql: "SELECT * FROM TransactionQueue ORDER BY dateAdded, id",
  2360. result: function(data){
  2361. // pre-process what we have here into a queue of functions to execute.
  2362. if(data && data.length){
  2363. actions.push(function(){
  2364. qd.services.online.onSyncItemStart("Fetching disc queue");
  2365. methods.discs().addCallback(function(){
  2366. qd.services.online.onSyncItemComplete();
  2367. }).addErrback(function(err){
  2368. console.warn(err);
  2369. qd.services.online.onSyncItemComplete();
  2370. });
  2371. });
  2372. actions.push(function(){
  2373. qd.services.online.onSyncItemStart("Fetching instant queue");
  2374. methods.instant().addCallback(function(){
  2375. qd.services.online.onSyncItemComplete();
  2376. }).addErrback(function(err){
  2377. console.warn(err);
  2378. qd.services.online.onSyncItemComplete();
  2379. });
  2380. });
  2381. dojo.forEach(data, function(item){
  2382. var method = item.method;
  2383. actions.push(function(){
  2384. if(methods[method]){
  2385. try {
  2386. qd.services.online.onSyncItemStart(item.prompt);
  2387. methods[method](dojo.fromJson(item.args||"{}")).addCallback(function(){
  2388. qd.services.online.onSyncItemComplete();
  2389. }).addErrback(function(err){
  2390. console.warn(err);
  2391. qd.services.online.onSyncItemComplete();
  2392. });
  2393. } catch(ex){
  2394. console.warn("sync: ", ex);
  2395. }
  2396. }
  2397. });
  2398. });
  2399. qd.services.online.onSyncNeeded(actions.length);
  2400. }
  2401. }
  2402. });
  2403. }
  2404. function execute(){
  2405. // note that the setTimeout is there to limit the number of
  2406. // things posted to Netflix to 4 a second.
  2407. var fn = actions.shift();
  2408. if(fn){
  2409. setTimeout(function(){
  2410. fn();
  2411. }, 400);
  2412. } else {
  2413. // we are done, so wipe the transaction queue.
  2414. db.execute({
  2415. sql: "DELETE FROM TransactionQueue"
  2416. });
  2417. qd.services.online.onSyncComplete();
  2418. }
  2419. }
  2420. // connect to the onChange event of the network.
  2421. dojo.connect(network, "onChange", function(){
  2422. if(network.available){
  2423. // check to see if there's anything stored in the transaction queue.
  2424. getActions();
  2425. }
  2426. });
  2427. //////////////////////////////////////////////////////////////////////////////////////////////
  2428. dojo.mixin(qd.services.online, {
  2429. process: function(titles, dfd){
  2430. // summary:
  2431. // Process a list of titles by both saving images, and
  2432. // saving the title itself into the database.
  2433. // titles: Array | Object
  2434. // The title(s) to be processed
  2435. // dfd: dojo.Deferred?
  2436. // An optional deferred to be fired during the processing.
  2437. // returns: void
  2438. var util = qd.services.util,
  2439. proc = titles;
  2440. if(!(titles instanceof Array)){ proc = [ titles ]; }
  2441. // pre-process image urls.
  2442. dojo.forEach(proc, function(item){
  2443. item.art.large = util.image.url(item.art.large);
  2444. });
  2445. // do the callback
  2446. if(dfd){ dfd.callback(titles); }
  2447. // post-process with a timeout, let the UI finish rendering and try
  2448. // to let it finish pulling images, since it looks like when we do
  2449. // this, AIR saves it off the cache anyways.
  2450. setTimeout(function(){
  2451. dojo.forEach(proc, function(item){
  2452. if(item.art.large && item.art.large.indexOf("http://")==0){
  2453. var imageDfd = util.image.store(item.art.large);
  2454. imageDfd.addCallback(function(u){
  2455. item.art.large = u;
  2456. });
  2457. }
  2458. });
  2459. }, 2000);
  2460. },
  2461. onSyncNeeded: function(/* Integer */n){
  2462. // summary:
  2463. // stub for connecting to for the sync process.
  2464. },
  2465. synchronize: function(){
  2466. // summary:
  2467. // Start the synchronization process.
  2468. execute();
  2469. },
  2470. onSyncComplete: function(){
  2471. // summary:
  2472. // Stub for when the synchronization process is complete.
  2473. },
  2474. onSyncItemStart: function(/* String */prompt){
  2475. // summary:
  2476. // Stub for when an item is about to be executed.
  2477. },
  2478. onSyncItemComplete: function(){
  2479. // summary:
  2480. // Stub for each item completion.
  2481. execute();
  2482. },
  2483. discardSynchronizations: function(){
  2484. // summary:
  2485. // Throw out any stored sync.
  2486. db.execute({
  2487. sql: "DELETE FROM TransactionQueue",
  2488. result: function(data){
  2489. qd.services.online.onDiscardSync();
  2490. }
  2491. });
  2492. },
  2493. onDiscardSync: function(){
  2494. // summary:
  2495. // Stub for when sync actions are discarded.
  2496. }
  2497. });
  2498. })();
  2499. }
  2500. if(!dojo._hasResource["qd.services.offline.feeds"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  2501. dojo._hasResource["qd.services.offline.feeds"] = true;
  2502. dojo.provide("qd.services.offline.feeds");
  2503. (function(){
  2504. var ps = qd.services.parser,
  2505. db = qd.services.data,
  2506. util = qd.services.util;
  2507. // private function to handle all feeds
  2508. var rssFeeds = {
  2509. top25: [],
  2510. top100: null,
  2511. newReleases: null
  2512. };
  2513. var top25Feeds = [], top100Feed, newReleasesFeed;
  2514. var feedlistInit = function(){
  2515. db.fetch({
  2516. sql: "SELECT id, term, lastUpdated, feed, xml FROM GenreFeed ORDER BY term",
  2517. result: function(data, result){
  2518. dojo.forEach(data, function(item){
  2519. if(item.feed.indexOf("Top100RSS")>-1){
  2520. rssFeeds.top100 = item;
  2521. } else if(item.feed.indexOf("NewReleasesRSS")>-1){
  2522. rssFeeds.newReleases = item;
  2523. } else {
  2524. rssFeeds.top25.push(item);
  2525. }
  2526. });
  2527. }
  2528. });
  2529. };
  2530. if(db.initialized){
  2531. feedlistInit();
  2532. } else {
  2533. var h = dojo.connect(db, "onInitialize", function(){
  2534. dojo.disconnect(h);
  2535. feedlistInit();
  2536. });
  2537. }
  2538. function getFeedObject(url){
  2539. if(url == rssFeeds.top100.feed){ return rssFeeds.top100; }
  2540. if(url == rssFeeds.newReleases.feed){ return rssFeeds.newReleases; }
  2541. for(var i=0; i<rssFeeds.top25.length; i++){
  2542. if(url == rssFeeds.top25[i].feed){
  2543. return rssFeeds.top25[i];
  2544. }
  2545. }
  2546. return null;
  2547. }
  2548. dojo.mixin(qd.services.offline.feeds, {
  2549. // summary:
  2550. // The offline-based service for fetching cached Netflix public RSS feeds.
  2551. list: function(){
  2552. // summary:
  2553. // Return the list of top 25 feeds.
  2554. return rssFeeds.top25;
  2555. },
  2556. top100: function(){
  2557. // summary:
  2558. // Return the top 100 feed.
  2559. return rssFeeds.top100;
  2560. },
  2561. newReleases: function(){
  2562. // summary:
  2563. // Return the New Releases feed.
  2564. return rssFeeds.newReleases;
  2565. },
  2566. fetch: function(/* qd.services.online.feeds.fetch.__FetchArgs */kwArgs){
  2567. // summary:
  2568. // Fetch the given feed information out of the database cache.
  2569. var dfd = util.prepare(kwArgs), feed = getFeedObject(kwArgs.url);
  2570. if(feed && feed.xml){
  2571. var xml = new DOMParser().parseFromString(feed.xml, "text/xml");
  2572. var node, parsed = [], items = xml.evaluate("//channel/item", xml);
  2573. while(node = items.iterateNext()){
  2574. var item = ps.titles.fromRss(node);
  2575. item.art.large = util.image.url(item.art.large);
  2576. parsed.push(item);
  2577. }
  2578. dfd.callback(parsed);
  2579. } else {
  2580. dfd.errback(new Error("qd.service.feeds.fetch: there is no XML cache for this feed."), feed.term);
  2581. }
  2582. return dfd; // dojo.Deferred
  2583. }
  2584. });
  2585. })();
  2586. }
  2587. if(!dojo._hasResource["qd.services.offline.titles"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  2588. dojo._hasResource["qd.services.offline.titles"] = true;
  2589. dojo.provide("qd.services.offline.titles");
  2590. (function(){
  2591. var ps = qd.services.parser,
  2592. db = qd.services.data,
  2593. util = qd.services.util;
  2594. dojo.mixin(qd.services.offline.titles, {
  2595. // summary:
  2596. // The offline-based service to pull title information (movies, TV shows, etc.) out of the cache.
  2597. save: function(/* Object */item){
  2598. // summary:
  2599. // Save the passed item. Redundant but necessary to not break app code.
  2600. qd.services.online.titles.save(item);
  2601. },
  2602. clear: function(){
  2603. // summary:
  2604. // Clear all of the titles out of the cache.
  2605. qd.services.online.titles.clear();
  2606. },
  2607. find: function(/* qd.services.online.titles.find.__FindArgs */kwArgs){
  2608. // summary:
  2609. // Try to pull as many titles from the database as possible,
  2610. // based on the terms.
  2611. var dfd = util.prepare(kwArgs);
  2612. var sql = "SELECT t.title, t.json "
  2613. + "FROM Title t "
  2614. + "INNER JOIN (SELECT DISTINCT title FROM Title WHERE SUBSTR(title, 0, :length) = :term AND json IS NOT NULL) t1 "
  2615. + "ON t1.title = t.title "
  2616. + "INNER JOIN (SELECT DISTINCT title FROM Title WHERE title LIKE :like AND json IS NOT NULL) t2 "
  2617. + "ON t2.title = t.title "
  2618. + "ORDER BY t.title";
  2619. // + " LIMIT " + Math.min((kwArgs.max || 25), 100) + "," + (kwArgs.start || 0);
  2620. db.execute({
  2621. sql: sql,
  2622. params: {
  2623. length: kwArgs.term.length,
  2624. term: kwArgs.term,
  2625. like: "%" + kwArgs.term + "%"
  2626. },
  2627. result: function(data, result){
  2628. var o = {
  2629. number_found: data && data.length || 0,
  2630. search_term: kwArgs.term,
  2631. results: [],
  2632. sort_by: "Relevance"
  2633. };
  2634. if(data && data.length){
  2635. for(var i=0; i<data.length; i++){
  2636. var item = dojo.fromJson(data[i].json);
  2637. item.art.large = util.image.url(item.art.large);
  2638. o.results.push(item);
  2639. qd.services.item(item);
  2640. }
  2641. dfd.callback(o);
  2642. } else {
  2643. console.warn("titles.find ERROR:", o);
  2644. dfd.errback(o);
  2645. }
  2646. },
  2647. error: function(result){
  2648. console.warn("titles.find ERROR:", result);
  2649. dfd.errback(result);
  2650. }
  2651. });
  2652. return dfd; // dojo.Deferred
  2653. },
  2654. fetch: function(/* qd.services.online.titles.fetch.__FetchArgs */kwArgs){
  2655. // summary:
  2656. // Fetch full title information out the cache and return it.
  2657. var dfd = util.prepare(kwArgs);
  2658. // Check the cache first.
  2659. var sql = "SELECT * FROM Title WHERE guid=:guid AND json IS NOT NULL",
  2660. params = { guid: kwArgs.guid };
  2661. if(kwArgs.term){
  2662. sql = "SELECT * FROM Title WHERE title=:term AND json IS NOT NULL",
  2663. params = { term: kwArgs.term };
  2664. }
  2665. db.fetch({
  2666. sql: sql,
  2667. params: params,
  2668. result: function(data, result){
  2669. var title;
  2670. if(data && data.length){
  2671. title = dojo.fromJson(data[0].json);
  2672. title.art.large = util.image.url(title.art.large);
  2673. qd.services.item(item);
  2674. }
  2675. setTimeout(function(){
  2676. if(title){
  2677. dfd.callback(title, result);
  2678. } else {
  2679. dfd.errback(new Error("qd.offline.service.fetch: the title '" + kwArgs.term + "' is unavailable."));
  2680. }
  2681. }, 10);
  2682. }
  2683. });
  2684. return dfd; // dojo.Deferred
  2685. },
  2686. autosuggest: function(/* on.titles.autosuggest.__AutosuggestArgs */kwArgs){
  2687. // summary:
  2688. // Return up to 10 terms out of the cache that sort of match the passed string.
  2689. var dfd = util.prepare(kwArgs);
  2690. var sql = "SELECT 1 AS main, title FROM Title WHERE SUBSTR(title, 0, :length) = :term "
  2691. + "UNION SELECT 2 AS main, title FROM Title WHERE title LIKE :like "
  2692. + "ORDER BY main, title LIMIT 0,10";
  2693. db.fetch({
  2694. sql: sql,
  2695. params: {
  2696. length: kwArgs.term.length,
  2697. term: kwArgs.term,
  2698. like: "%" + kwArgs.term + "%"
  2699. },
  2700. result: function(data){
  2701. var a = [];
  2702. dojo.forEach(data, function(item){
  2703. a.push(item.title);
  2704. });
  2705. setTimeout(function(){
  2706. dfd.callback(a, null);
  2707. }, 10);
  2708. }
  2709. });
  2710. return dfd; // dojo.Deferred
  2711. },
  2712. rated: function(/* qd.services.online.titles.rated.__RatedArgs */kwArgs){
  2713. // summary:
  2714. // Return any cached ratings info based on the passed set of title guids.
  2715. var dfd = new dojo.Deferred();
  2716. db.execute({
  2717. sql: "SELECT json FROM Title WHERE guid IN ('" + (kwArgs.guids || []).join("','") + "') ORDER BY title",
  2718. result: function(data){
  2719. if(data && data.length){
  2720. var a = [];
  2721. dojo.forEach(data, function(item){
  2722. var tmp = dojo.fromJson(item.json);
  2723. if(tmp.ratings && tmp.ratings.predicted){
  2724. tmp.art.large = util.image.url(tmp.art.large);
  2725. qd.services.item(tmp);
  2726. a.push(tmp);
  2727. }
  2728. });
  2729. if(a.length){
  2730. kwArgs.result.call(kwArgs, a);
  2731. }
  2732. }
  2733. // no matter what happens, run the callback.
  2734. dfd.callback(a);
  2735. }
  2736. });
  2737. return dfd; // dojo.Deferred
  2738. },
  2739. rate: function(/* qd.services.online.titles.rate.__RateArgs */kwArgs){
  2740. // summary:
  2741. // Store the passed rating command in the transaction queue
  2742. // for synchronization purposes.
  2743. var dfd = util.prepare(kwArgs),
  2744. item = qd.services.item(kwArgs.guid);
  2745. if(!item){
  2746. setTimeout(function(){
  2747. dfd.errback(new Error("qd.service.rate: cannot rate an item with a guid of " + kwArgs.guid));
  2748. }, 10);
  2749. return dfd;
  2750. }
  2751. var sql = "INSERT INTO TransactionQueue (method, args, prompt, dateAdded) "
  2752. + " VALUES (:method, :args, :prompt, DATETIME())";
  2753. var params = {
  2754. method: "rate",
  2755. args: "{guid:'" + kwArgs.guid + "',rating:'" + kwArgs.rating + "'}",
  2756. prompt: 'Setting the rating on "' + item.title + '" to ' + kwArgs.rating
  2757. };
  2758. db.execute({
  2759. sql: sql,
  2760. params: params,
  2761. result: function(data){
  2762. // just do the callback.
  2763. dfd.callback(kwArgs.rating);
  2764. }
  2765. });
  2766. return dfd; // dojo.Deferred
  2767. },
  2768. recommendations: function(/* qd.services.titles.online.recommendations.__RecArgs */kwArgs){
  2769. // summary:
  2770. // Get any recommendations out of the cache and return them.
  2771. var dfd = util.prepare(kwArgs),
  2772. sql = "SELECT DISTINCT r.guid AS guid, r.title AS title, t.json AS json "
  2773. + "FROM Recommendation r "
  2774. + "INNER JOIN Title t "
  2775. + "ON t.guid = r.guid "
  2776. + "WHERE t.json IS NOT NULL "
  2777. + "ORDER BY r.title",
  2778. max = kwArgs.max || 25,
  2779. start = kwArgs.start || 0;
  2780. sql += " LIMIT " + start + "," + max;
  2781. db.execute({
  2782. sql: sql,
  2783. result: function(data){
  2784. // A note: we are not going to throw any errors if there's nothing
  2785. // in the database that's cached.
  2786. var a = [];
  2787. if(data && data.length){
  2788. dojo.forEach(data, function(item){
  2789. var title = dojo.fromJson(item.json);
  2790. title.art.large = util.image.url(title.art.large);
  2791. a.push(title);
  2792. qd.services.item(title);
  2793. });
  2794. }
  2795. dfd.callback(a);
  2796. },
  2797. error: function(data){
  2798. dfd.errback(data);
  2799. }
  2800. });
  2801. return dfd; // dojo.Deferred
  2802. }
  2803. });
  2804. })();
  2805. }
  2806. if(!dojo._hasResource["qd.services.offline.queues"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  2807. dojo._hasResource["qd.services.offline.queues"] = true;
  2808. dojo.provide("qd.services.offline.queues");
  2809. (function(){
  2810. var ps = qd.services.parser,
  2811. db = qd.services.data,
  2812. util = qd.services.util;
  2813. dojo.mixin(qd.services.offline.queues, {
  2814. // summary:
  2815. // The offline-based service for queue information. All of this runs off the internal cache.
  2816. etag: function(/* String */queue, /* String? */tag){
  2817. // summary:
  2818. // Get the last etag for the specified queue.
  2819. return qd.services.online.queues.etag(queue, tag); // String
  2820. },
  2821. clear: function(){
  2822. // summary:
  2823. // Clear out the queue cache.
  2824. qd.services.online.queues.clear();
  2825. },
  2826. fetch: function(/* qd.services.online.queues.fetch.__FetchArgs */kwArgs){
  2827. // summary:
  2828. // Fetch the queue information based on the passed partial URL.
  2829. var dfd = util.prepare(kwArgs);
  2830. var sql = "SELECT * FROM QueueCache WHERE queue=:queue",
  2831. queue = "disc";
  2832. if(kwArgs.url == "queues/disc/saved"){ queue = "saved"; }
  2833. else if(kwArgs.url.indexOf("queues/instant")>-1){ queue = "instant"; }
  2834. else if(kwArgs.url == "at_home"){ queue = "at_home"; }
  2835. else if(kwArgs.url == "rental_history/watched"){ queue = "watched"; }
  2836. else if(kwArgs.url.indexOf("rental_history")>-1){ queue = "history"; }
  2837. db.execute({
  2838. sql: sql,
  2839. params: {
  2840. queue: queue
  2841. },
  2842. result: function(data){
  2843. // pre-process and send back.
  2844. var a = [];
  2845. if(data && data.length){
  2846. a = dojo.fromJson(data[0].json);
  2847. dojo.forEach(a, function(item){
  2848. item.title.art.large = util.image.url(item.title.art.large);
  2849. item.title.art.small = util.image.url(item.title.art.small);
  2850. qd.services.item(item);
  2851. qd.services.item(item.title);
  2852. });
  2853. }
  2854. dfd.callback(a);
  2855. },
  2856. error: function(data){
  2857. dfd.errback(data);
  2858. }
  2859. });
  2860. return dfd; // dojo.Deferred
  2861. },
  2862. atHome: function(/* qd.services.online.queues.fetch.__FetchArgs */kwArgs){
  2863. // summary:
  2864. // Fetch the user's At Home queue.
  2865. kwArgs = kwArgs || {};
  2866. kwArgs.url = "at_home";
  2867. var dfd = this.fetch(kwArgs);
  2868. dfd.addCallback(function(arr){
  2869. console.log("offline at home: ", arr);
  2870. });
  2871. return dfd; // dojo.Deferred
  2872. },
  2873. discs: function(/* qd.services.online.queues.fetch.__FetchArgs */kwArgs){
  2874. // summary:
  2875. // Fetch the user's disc queue.
  2876. kwArgs = kwArgs || {};
  2877. kwArgs.url = "queues/disc";
  2878. var dfd = this.fetch(kwArgs);
  2879. dfd.addCallback(function(arr){
  2880. // console.log("discs: ", arr);
  2881. });
  2882. return dfd; // dojo.Deferred
  2883. },
  2884. saved: function(/* qd.services.online.queues.fetch.__FetchArgs */kwArgs){
  2885. // summary:
  2886. // Fetch the user's saved discs.
  2887. kwArgs = kwArgs || {};
  2888. kwArgs.url = "queues/disc/saved";
  2889. var dfd = this.fetch(kwArgs);
  2890. dfd.addCallback(function(arr){
  2891. // console.log("saved discs: ", arr);
  2892. });
  2893. return dfd; // dojo.Deferred
  2894. },
  2895. instant: function(/* qd.services.online.queues.fetch.__FetchArgs */kwArgs){
  2896. // summary:
  2897. // Fetch the user's instant queue.
  2898. kwArgs = kwArgs || {};
  2899. kwArgs.url = "queues/instant";
  2900. var dfd = this.fetch(kwArgs);
  2901. dfd.addCallback(function(arr){
  2902. // console.log("instant: ", arr);
  2903. });
  2904. return dfd; // dojo.Deferred
  2905. },
  2906. watched: function(/* qd.services.online.queues.fetch.__FetchArgs */kwArgs){
  2907. // summary:
  2908. // Fetch the user's instant watched queue.
  2909. kwArgs = kwArgs || {};
  2910. kwArgs.url = "rental_history/watched";
  2911. var dfd = this.fetch(kwArgs);
  2912. dfd.addCallback(function(arr){
  2913. // console.log("watched: ", arr);
  2914. });
  2915. return dfd; // dojo.Deferred
  2916. },
  2917. shipped: function(/* qd.services.online.queues.fetch.__FetchArgs */kwArgs){
  2918. // summary:
  2919. // Fetch the user's shipped disc history.
  2920. kwArgs = kwArgs || {};
  2921. kwArgs.url = "rental_history/shipped";
  2922. var dfd = this.fetch(kwArgs);
  2923. dfd.addCallback(function(arr){
  2924. console.log("shipped: ", arr);
  2925. });
  2926. return dfd; // dojo.Deferred
  2927. },
  2928. returned: function(/* qd.services.online.queues.fetch.__FetchArgs */kwArgs){
  2929. // summary:
  2930. // Fetch the user's returned disc history.
  2931. kwArgs = kwArgs || {};
  2932. kwArgs.url = "rental_history/returned";
  2933. var dfd = this.fetch(kwArgs);
  2934. dfd.addCallback(function(arr){
  2935. console.log("returned: ", arr);
  2936. });
  2937. return dfd; // dojo.Deferred
  2938. },
  2939. modify: function(/* qd.services.online.queues.add.__ModifyArgs */kwArgs){
  2940. // summary:
  2941. // Store the passed action in the transaction queue for sync when returning
  2942. // online.
  2943. var dfd = util.prepare(kwArgs),
  2944. item = qd.services.item(kwArgs.guid),
  2945. title = kwArgs.title || "";
  2946. if(kwArgs.position === undefined){
  2947. // this is an add. In theory we should never get here.
  2948. setTimeout(function(){
  2949. dfd.callback({});
  2950. }, 10);
  2951. return dfd; // dojo.Deferred
  2952. }
  2953. var sql = "INSERT INTO TransactionQueue (method, args, prompt, dateAdded) "
  2954. + " VALUES (:method, :args, :prompt, DATETIME())";
  2955. var args = {
  2956. url: kwArgs.url || "queues/disc",
  2957. guid: kwArgs.guid,
  2958. title: kwArgs.title,
  2959. position: kwArgs.position
  2960. };
  2961. if(kwArgs.format !== undefined){
  2962. args.format = kwArgs.format;
  2963. }
  2964. var params = {
  2965. method: "modify",
  2966. args: dojo.toJson(args),
  2967. prompt: 'Modifying queue item "' + title + '"'
  2968. };
  2969. db.execute({
  2970. sql: sql,
  2971. params: params,
  2972. result: function(data){
  2973. // need to set up a fake status object, depends on how this
  2974. // method was called.
  2975. var obj = {};
  2976. obj.code = "201";
  2977. var i = dojo.clone(item);
  2978. i.guid = i.id.substr(0, lastIndexOf("/")) + kwArgs.position + i.id.substr(lastIndexOf("/")+1);
  2979. i.position = kwArgs.position;
  2980. obj.created = [ i ];
  2981. dfd.callback(obj);
  2982. }
  2983. });
  2984. return dfd; // dojo.Deferred
  2985. },
  2986. remove: function(/* qd.services.online.queues.remove.__RemoveArgs */kwArgs){
  2987. // summary:
  2988. // Store the remove queue item command in the transaction queue for later
  2989. // sync when the user returns online.
  2990. var dfd = util.prepare(kwArgs);
  2991. var sql = "INSERT INTO TransactionQueue (method, args, prompt, dateAdded) "
  2992. + " VALUES (:method, :args, :prompt, DATETIME())";
  2993. db.execute({
  2994. sql: sql,
  2995. params: {
  2996. method: "remove",
  2997. args: dojo.toJson({
  2998. url: kwArgs.url,
  2999. guid: kwArgs.guid,
  3000. title: kwArgs.title
  3001. }),
  3002. prompt: 'Removing "' + (kwArgs.title||"") + '" from the ' + (kwArgs.url.indexOf("instant")>-1?"instant":"disc") + " queue"
  3003. },
  3004. result: function(data){
  3005. dfd.callback({ code: 200 });
  3006. }
  3007. });
  3008. return dfd; // dojo.Deferred
  3009. },
  3010. cache: function(){ return; },
  3011. addMovieById: function(/* String */movieId, target, /* String */queue){
  3012. // summary:
  3013. // Interceptor function to store the transaction to be executed
  3014. // upon returning online.
  3015. queue = queue || "queue";
  3016. var item = qd.services.item(movieId);
  3017. if(!item){
  3018. console.warn("Can't add movie: it doesn't have full information here yet.", movieId);
  3019. return;
  3020. }
  3021. var sql = "INSERT INTO TransactionQueue (method, args, prompt, dateAdded) "
  3022. + " VALUES (:method, :args, :prompt, DATETIME())";
  3023. db.execute({
  3024. sql: sql,
  3025. params: {
  3026. method: "add",
  3027. args: dojo.toJson({
  3028. movieId: movieId,
  3029. queue: queue
  3030. }),
  3031. prompt: 'Adding "' + item.title + '" to the ' + (queue=="queue"?"disc":"instant") + " queue"
  3032. },
  3033. result: function(data){
  3034. console.log("addMovieById storage complete.");
  3035. }
  3036. });
  3037. },
  3038. addMovieByTerm: function(/* String */term, target, /* String */queue){
  3039. // summary:
  3040. // Interceptor function to store the transaction to be executed
  3041. // upon returning online.
  3042. queue = queue || "queue";
  3043. var sql = "INSERT INTO TransactionQueue (method, args, prompt, dateAdded) "
  3044. + " VALUES (:method, :args, :prompt, DATETIME())";
  3045. db.execute({
  3046. sql: sql,
  3047. params: {
  3048. method: "termAdd",
  3049. args: dojo.toJson({
  3050. term: term,
  3051. queue: queue
  3052. }),
  3053. prompt: 'Adding "' + term + '" to the ' + (queue=="queue"?"disc":"instant") + " queue"
  3054. },
  3055. result: function(data){
  3056. console.log("addMovieByTerm storage complete.");
  3057. }
  3058. });
  3059. }
  3060. });
  3061. })();
  3062. }
  3063. if(!dojo._hasResource["qd.services.offline.user"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  3064. dojo._hasResource["qd.services.offline.user"] = true;
  3065. dojo.provide("qd.services.offline.user");
  3066. (function(){
  3067. dojo.mixin(qd.services.offline.user, {
  3068. // summary:
  3069. // Return the user information out of the cache.
  3070. fetch: function(){
  3071. // summary:
  3072. // Return the cached user object if we are authorized.
  3073. var dfd = new dojo.Deferred();
  3074. var fn = function(){
  3075. dfd.errback(new Error("qd.service.user.fetch: you must authorize the application to fetch user details."));
  3076. };
  3077. setTimeout(function(){
  3078. dfd.callback(qd.app.user());
  3079. }, 10);
  3080. return dfd; // dojo.Deferred
  3081. }
  3082. });
  3083. })();
  3084. }
  3085. if(!dojo._hasResource["qd.services.offline"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  3086. dojo._hasResource["qd.services.offline"] = true;
  3087. dojo.provide("qd.services.offline");
  3088. }
  3089. if(!dojo._hasResource["qd.services.authorization"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  3090. dojo._hasResource["qd.services.authorization"] = true;
  3091. dojo.provide("qd.services.authorization");
  3092. (function(){
  3093. var auth = qd.services.authorization;
  3094. auth.request = function(){
  3095. // summary:
  3096. // In the event of a new user, we need to request a user's access token. This is
  3097. // done through a non-standard handshake process. Based on Mike Wilcox's original
  3098. // oauth handshake.
  3099. var dfd = new dojo.Deferred(),
  3100. token = qd.app.authorization;
  3101. var kwArgs = {
  3102. url: "http://api.netflix.com/oauth/request_token",
  3103. handleAs: "text",
  3104. error: function(err, ioArgs){
  3105. dfd.errback("auth", ioArgs.xhr.responseText);
  3106. },
  3107. load: function(response, ioArgs){
  3108. // this should force us to open the window for the Netflix auth handshake.
  3109. var a = response.split("&"),
  3110. o = {};
  3111. dojo.forEach(a, function(item){
  3112. var pair = item.split("=");
  3113. o[pair[0]] = unescape(pair[1]);
  3114. });
  3115. var url = "http://api-user.netflix.com/oauth/login?"
  3116. + "application_name=" + o.application_name
  3117. + "&oauth_consumer_key=" + token.consumer.key
  3118. + "&oauth_token=" + o.oauth_token;
  3119. // temporarily set the token for the second part of the handshake.
  3120. token.token = {
  3121. key: o.oauth_token,
  3122. secret: o.oauth_token_secret
  3123. };
  3124. // open up the new window for authorization
  3125. var win1 = new dair.Window({
  3126. size: { h: 525, w: 350, t: 100, l: 100 },
  3127. href: url,
  3128. resizable: false,
  3129. alwaysInFront: true,
  3130. maximizable: true,
  3131. minimixeable: false
  3132. });
  3133. // add event listeners on the window.
  3134. var seenOnce = false;
  3135. var v = setInterval(function(){
  3136. var wurl = win1._window.location;
  3137. if(wurl != url){
  3138. if(!seenOnce && wurl=="https://api-user.netflix.com/oauth/login"){
  3139. seenOnce = true;
  3140. return;
  3141. }
  3142. else if(wurl=="http://www.netflix.com/TermsOfUse"){
  3143. //looking at the terms of use. Don't know how to get them back.
  3144. return;
  3145. }
  3146. else if(wurl.indexOf("Failed")>0){
  3147. // TODO: fire off the errback and kill the timer?
  3148. return;
  3149. }
  3150. clearInterval(v);
  3151. v = null;
  3152. win1.close();
  3153. }
  3154. }, 1000);
  3155. var c2 = dojo.connect(win1, "onClose", function(){
  3156. if(v){
  3157. dfd.errback("user");
  3158. clearInterval(v);
  3159. dojo.disconnect(c2);
  3160. return;
  3161. }
  3162. // we're good to go, so go get the access token
  3163. dojo.xhrGet(dojox.io.OAuth.sign("GET", {
  3164. url: "http://api.netflix.com/oauth/access_token",
  3165. handleAs: "text",
  3166. error: function(err, ioArgs){
  3167. dfd.errback("auth");
  3168. },
  3169. load: function(response, ioArgs){
  3170. var a = response.split("&"), o = {};
  3171. dojo.forEach(a, function(item){
  3172. var p = item.split("=");
  3173. o[p[0]] = unescape(p[1]);
  3174. });
  3175. qd.app.authorize(o.oauth_token, o.oauth_token_secret, o.user_id);
  3176. dfd.callback(o.user_id); // original used the username, should see if we can grab that.
  3177. }
  3178. }, token), false);
  3179. });
  3180. }
  3181. };
  3182. dojo.xhrGet(dojox.io.OAuth.sign("GET", kwArgs, token), false);
  3183. return dfd; // dojo.Deferred
  3184. };
  3185. })();
  3186. }
  3187. if(!dojo._hasResource["qd.services"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  3188. dojo._hasResource["qd.services"] = true;
  3189. dojo.provide("qd.services");
  3190. // This file is primarily a package loader, and initializer.
  3191. (function(){
  3192. var storage = qd.services.storage,
  3193. network = qd.services.network,
  3194. db = "queued.db",
  3195. dbProp = "OUhxbVZ1Mtmu4zx9LzS5cA==",
  3196. pwd;
  3197. dojo.connect(storage, "onClear", function(){
  3198. // push the database access info back into storage. Basically if we don't have
  3199. // the password, probably we're at re-initializing everything.
  3200. if(pwd){
  3201. storage.item(dbProp, pwd);
  3202. }
  3203. });
  3204. qd.services._forceCreate = false;
  3205. qd.services.init = function(){
  3206. // summary:
  3207. // Initialize the Queued services.
  3208. qd.app.splash("Getting database password");
  3209. pwd = storage.item(dbProp);
  3210. if(!pwd){
  3211. qd.app.splash("Generating database password");
  3212. // generate a new password for the database service and store it.
  3213. var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*~?0123456789-_abcdefghijklmnopqrstuvwxyz",
  3214. key = "";
  3215. for(var i=0; i<16; i++){
  3216. key += tab.charAt(Math.round(Math.random()*tab.length));
  3217. }
  3218. pwd = storage.item(dbProp, key);
  3219. qd.app.splash("Password generated (" + pwd.length + ")");
  3220. }
  3221. qd.app.splash("Initializing network monitor");
  3222. qd.services.network.start();
  3223. qd.app.splash("Initializing database services");
  3224. qd.services.data.init(pwd, db, qd.services._forceCreate);
  3225. };
  3226. var registry = {}, titleRegistry = {};
  3227. qd.services.item = function(/* String | Object */item){
  3228. // summary:
  3229. // Registry operations. If item is a string,
  3230. // this acts as a getter. If it is an object,
  3231. // it acts as a setter. Note that this should
  3232. // work for *any* object in the system, not just
  3233. // titles.
  3234. if(dojo.isString(item)){
  3235. return registry[item] || null; // Object
  3236. }
  3237. // assume it's an object.
  3238. if(item && !item.guid){
  3239. console.warn("qd.services.item: the passed item has no guid!", item);
  3240. return null; // Object
  3241. }
  3242. var tmp = registry[item.guid];
  3243. if(tmp){
  3244. item = qd.services.util.mixin(tmp, item);
  3245. }
  3246. registry[item.guid] = item;
  3247. if(item.title){
  3248. titleRegistry[item.title] = item;
  3249. }
  3250. return item; // Object
  3251. };
  3252. qd.services.itemByTerm = function(/* String */term){
  3253. // summary:
  3254. // Find any objects in the registry based on a title.
  3255. // If found, return it.
  3256. return titleRegistry[term]; // Object
  3257. };
  3258. qd.services.clearItems = function(){
  3259. // summary:
  3260. // Clear out any in-memory items that have been cached.
  3261. registry = {};
  3262. titleRegistry = {};
  3263. };
  3264. })();
  3265. }
  3266. if(!dojo._hasResource["qd.app"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  3267. dojo._hasResource["qd.app"] = true;
  3268. dojo.provide("qd.app");
  3269. qd.app = new (function(){
  3270. // native application references
  3271. var _app = air.NativeApplication.nativeApplication,
  3272. self = this;
  3273. // BEGIN APPLICATION-SPECIFIC EVENTS
  3274. _app.idleThreshold = 300;
  3275. this.splash = function(msg){
  3276. dojo.byId("splashMessage").innerHTML = msg + "...";
  3277. };
  3278. // Application-specific information
  3279. this.__defineGetter__("info", function(){
  3280. // summary:
  3281. // Get the application information and return
  3282. // it as a JSON object.
  3283. var xml = new DOMParser().parseFromString(_app.applicationDescriptor, "text/xml"),
  3284. root = xml.getElementsByTagName("application")[0],
  3285. copy = root.getElementsByTagName("copyright")[0],
  3286. ver = root.getElementsByTagName("version")[0];
  3287. var o = {
  3288. applicationId: _app.applicationID,
  3289. publisherId: _app.publisherID,
  3290. autoExit: _app.autoExit,
  3291. idleThreshold: _app.idleThreshold,
  3292. copyright: (copy && copy.firstChild && copy.firstChild.data) || null,
  3293. version: (ver && ver.firstChild && ver.firstChild.data) || null
  3294. };
  3295. return o; // Object
  3296. });
  3297. this.__defineGetter__("version", function(){
  3298. // summary:
  3299. // Return a float version of the version of Queued.
  3300. var info = this.info;
  3301. if(info.version){
  3302. return parseFloat(info.version, 10); // Float
  3303. }
  3304. return null; // Float
  3305. });
  3306. //
  3307. // cleanup functions
  3308. function onExit(evt){
  3309. // close all the windows and check to make sure they don't stop
  3310. // the event; if they don't, then go ahead and fire our own exiting
  3311. // event.
  3312. //air.trace("CLOSURE EXIT")
  3313. if(evt.isDefaultPrevented()){
  3314. return; // don't do anything
  3315. }
  3316. self.onExit(evt);
  3317. if(evt.isDefaultPrevented()){
  3318. return; // don't do anything
  3319. }
  3320. }
  3321. // since we are init, there should only be one open window.
  3322. window.nativeWindow.addEventListener(air.Event.CLOSING, onExit);
  3323. this.onExit = function(/* air.Event */evt){
  3324. // summary:
  3325. // Stub for handling any exiting events.
  3326. };
  3327. this.exit = function(){
  3328. // summary:
  3329. // Manually exit the application and call any finalizers.
  3330. // This *should* call our onExit handlers, above.
  3331. var evt = new air.Event(air.Event.EXITING, false, true);
  3332. _app.dispatchEvent(evt);
  3333. if(!evt.isDefaultPrevented()){
  3334. _app.exit();
  3335. }
  3336. };
  3337. // user idle functions
  3338. this.__defineGetter__("lastInput", function(){
  3339. // summary:
  3340. // The time, in seconds, since the last user input on the app.
  3341. return _app.timeSinceLastUserInput; // int
  3342. });
  3343. this.__defineGetter__("idleThreshold", function(){
  3344. // summary:
  3345. // Return how long the app will wait (in seconds) before firing the onIdle event.
  3346. return _app.idleThreshold; // Integer
  3347. });
  3348. this.__defineSetter__("idleThreshold", function(/* Integer */n){
  3349. // summary:
  3350. // Set the idle threshold for the application.
  3351. _app.idleThreshold = n;
  3352. });
  3353. function onIdle(evt){
  3354. self.onIdle(evt);
  3355. }
  3356. function onPresent(evt){
  3357. self.onPresent(evt);
  3358. }
  3359. _app.addEventListener(air.Event.USER_IDLE, onIdle);
  3360. _app.addEventListener(air.Event.USER_PRESENT, onPresent);
  3361. this.onIdle = function(/* air.Event */evt){
  3362. // summary:
  3363. // Stub for handling when the user is idle.
  3364. console.log("qd.app.onIdle: FIRING");
  3365. };
  3366. this.onPresent = function(/* air.Event */evt){
  3367. // summary:
  3368. // Stub for handling when the user returns from being idle.
  3369. console.log("qd.app.onPresent: FIRING");
  3370. };
  3371. // checking to see if this is the first time running the app.
  3372. this.onUpgrade = function(/* Float */oldVersion, /* Float */newVersion){
  3373. // summary:
  3374. // Stub for when the application is upgraded
  3375. console.warn("Update detected! Upgrading to version " + newVersion);
  3376. this.splash("Upgrading Queued to version " + newVersion);
  3377. var file = air.File.applicationDirectory.resolvePath("js/updates/commands.js");
  3378. if(file.exists){
  3379. var fs = new air.FileStream();
  3380. fs.open(file, air.FileMode.READ);
  3381. var js = fs.readUTFBytes(fs.bytesAvailable);
  3382. fs.close();
  3383. eval(js);
  3384. if(Updater){
  3385. Updater.invoke(oldVersion, newVersion);
  3386. }
  3387. }
  3388. };
  3389. this.onFirstRun = function(info){
  3390. // summary:
  3391. // Stub for when the application is run for the first time
  3392. this.splash("Setting up Queued");
  3393. console.log("qd.app.onFirstRun!");
  3394. console.log(info);
  3395. };
  3396. (function(){
  3397. // look for the existence of a file.
  3398. var info = self.info,
  3399. version = parseFloat(info.version, 10),
  3400. file = air.File.applicationStorageDirectory.resolvePath("preferences/version.txt"),
  3401. doWrite = false;
  3402. if(file.exists){
  3403. // check to see the version matches.
  3404. var stream = new air.FileStream();
  3405. stream.open(file, air.FileMode.READ);
  3406. var content = parseFloat(stream.readUTFBytes(stream.bytesAvailable), 10);
  3407. stream.close();
  3408. if(content < version){
  3409. // we have an updated version.
  3410. self.onUpgrade(content, version);
  3411. doWrite = true;
  3412. }
  3413. } else {
  3414. // fire the onFirstRun event.
  3415. self.onFirstRun(info);
  3416. doWrite = true;
  3417. }
  3418. // finally, write the new file if needed.
  3419. if(doWrite){
  3420. var stream = new air.FileStream();
  3421. stream.open(file, air.FileMode.WRITE);
  3422. var content = stream.writeUTFBytes(version);
  3423. stream.close();
  3424. }
  3425. })();
  3426. // set up the application updater.
  3427. var updater;
  3428. dojo.addOnLoad(dojo.hitch(this, function(){
  3429. this.splash("Setting up the auto-update check");
  3430. try{
  3431. updater = new runtime.air.update.ApplicationUpdaterUI();
  3432. updater.configurationFile = new air.File("app:/updateConfig.xml");
  3433. updater.addEventListener("initialized", function(evt){
  3434. // let the app finish it's thing first, then go hit for updates.
  3435. setTimeout(function(){
  3436. updater.checkNow();
  3437. }, 15000);
  3438. });
  3439. updater.initialize();
  3440. } catch(ex){
  3441. this.splash("Auto-update setup failed");
  3442. // swallow this error; for some reason Linux doesn't like
  3443. // the application updater.
  3444. }
  3445. }));
  3446. // END APP EVENTS
  3447. // Authorization setup.
  3448. /*=====
  3449. qd.app.__TokenObject = function(key, secret){
  3450. // summary:
  3451. // A token object (key/secret pair) for use with OAuth-based services.
  3452. // key: String
  3453. // The public key assigned by the OAuth service.
  3454. // secret: String
  3455. // The private key assigned by the OAuth service.
  3456. this.key = key;
  3457. this.secret = secret;
  3458. };
  3459. qd.app.__AuthObject = function(consumer, token, userId, sig_method){
  3460. // summary:
  3461. // The token/authorization object used by Queued to make any
  3462. // requests to Netflix to access protected resources.
  3463. // consumer: qd.app.__TokenObject
  3464. // The key/secret pair assigned to Queued by Netflix.
  3465. // token: qd.app.__TokenObject?
  3466. // The key/secret pair assigned to the User by Netflix. Will
  3467. // be null if the user has not completed the authorization process.
  3468. // userId: String?
  3469. // The ID of the user as assigned by Netflix.
  3470. // sig_method: String?
  3471. // The signature method to be used by the OAuth service. HMAC-SHA1 is
  3472. // the default.
  3473. this.consumer = consumer;
  3474. this.token = token;
  3475. this.userId = userId;
  3476. this.sig_method = sig_method || "HMAC-SHA1";
  3477. }
  3478. =====*/
  3479. var acl;
  3480. this.__defineGetter__("authorization", function(){
  3481. // summary:
  3482. // Return the private authorization object for OAuth-based requests.
  3483. if(!acl){
  3484. acl = {
  3485. consumer: {
  3486. key:"6tuk26jpceh3z8d362suu2kd",
  3487. secret:"pRM4YDTtqD"
  3488. },
  3489. sig_method: "HMAC-SHA1",
  3490. token: null,
  3491. userId: null
  3492. };
  3493. qd.services.storage.item("token", acl);
  3494. }
  3495. return acl; // qd.app.__AuthObject
  3496. });
  3497. this.__defineGetter__("authorized", function(){
  3498. // summary:
  3499. // Return whether or not the current user is actually authorized.
  3500. // Replaces isLoggedIn().
  3501. var signer = this.authorization;
  3502. return (signer.token !== null && signer.userId !== null); // Boolean
  3503. });
  3504. this.authorize = function(/* String */token, /* String */secret, /* String */userId){
  3505. // summary
  3506. // Set the user's tokens on the ACO.
  3507. if(!token || !secret){
  3508. throw new Error("qd.app.authorize: you must pass the authorization information.");
  3509. }
  3510. var o = this.authorization;
  3511. // set the token properties
  3512. o.token = {
  3513. key: token,
  3514. secret: secret
  3515. };
  3516. // set the userId
  3517. o.userId = userId;
  3518. // drop it into storage.
  3519. qd.services.storage.item("token", o);
  3520. return o; // qd.app.__AuthObject
  3521. };
  3522. this.deauthorize = function(){
  3523. // summary:
  3524. // Remove the Netflix authorization tokens from the application's acl object.
  3525. var o = this.authorization;
  3526. o.token = null;
  3527. o.userId = null;
  3528. qd.services.storage.item("token", o);
  3529. // remove the user object from storage.
  3530. qd.app.user(null);
  3531. qd.service.titles.clear();
  3532. qd.service.queues.clear();
  3533. qd.services.clearItems();
  3534. return o; // qd.app.__AuthObject
  3535. };
  3536. // authorization initialization
  3537. dojo.addOnLoad(function(){
  3538. // try to get the current token out of storage.
  3539. try {
  3540. self.splash("Getting user token");
  3541. acl = qd.services.storage.item("token");
  3542. } catch(ex){
  3543. // swallow it.
  3544. self.splash("User token not found");
  3545. }
  3546. });
  3547. // User information
  3548. var user;
  3549. this.user = function(/* Object? */obj){
  3550. // summary:
  3551. // An object that represents in memory user information.
  3552. // If an object is passed, this acts as a setter; if not,
  3553. // it acts as a getter. If there is no user object in
  3554. // memory and it is called as a getter, this will retrieve
  3555. // it from local storage, if it exists.
  3556. if(obj!==undefined){
  3557. user = obj;
  3558. this.save();
  3559. return user; // Object
  3560. }
  3561. if(user){
  3562. return user; // Object
  3563. }
  3564. return user = qd.services.storage.item("user"); // Object
  3565. };
  3566. this.save = function(){
  3567. // summary:
  3568. // Store the user object into encrypted local storage.
  3569. var _s = new Date();
  3570. qd.services.storage.item("user", user);
  3571. console.warn("Time to save user info into storage: " + (new Date()-_s) + "ms.");
  3572. };
  3573. dojo.addOnLoad(function(){
  3574. var user = qd.app.user();
  3575. if(user){
  3576. dojo.byId("topNavUser").innerHTML = "Welcome " + user.name.first + " " + user.name.last;
  3577. dojo.byId("prefsUserName").innerHTML = user.name.first + " " + user.name.last;
  3578. }
  3579. else if(!user && qd.app.authorized){
  3580. // fetching the user information, since it seems to be missing.
  3581. var h = dojo.connect(qd.services.network, "onChange", function(){
  3582. dojo.disconnect(h);
  3583. var dfd = qd.service.user.fetch();
  3584. dfd.addCallback(function(obj){
  3585. qd.app.user(obj);
  3586. dojo.byId("topNavUser").innerHTML = "Welcome " + obj.name.first + " " + obj.name.last;
  3587. dojo.byId("prefsUserName").innerHTML = obj.name.first + " " + obj.name.last;
  3588. });
  3589. });
  3590. }
  3591. if(qd.app.authorized){
  3592. dojo.style("searchBar", "display", "block");
  3593. dojo.removeClass(dojo.body(), "notLoggedIn");
  3594. }
  3595. });
  3596. // view the source code.
  3597. this.source = function(){
  3598. // summary:
  3599. // Open the Adobe source code viewer so one can browse the source tree.
  3600. try {
  3601. var vs = air.SourceViewer.getDefault();
  3602. // Note that the following exclusions are aimed at a release, and not a debug session.
  3603. vs.setup({
  3604. exclude: [ '/lib', '/META-INF', 'mimetype', 'Queued.exe', 'Icon.icns' ],
  3605. colorScheme: 'nightScape'
  3606. });
  3607. vs.viewSource();
  3608. } catch(ex){
  3609. console.warn("You cannot run the source code viewer in debug mode.");
  3610. console.dir(ex);
  3611. }
  3612. };
  3613. /*=====
  3614. qd.app.underlay.__Args = function(loader, bodyOnly){
  3615. // summary:
  3616. // Keyword arguments object to be passed to qd.app.underlay.show.
  3617. // loader: Boolean?
  3618. // Specifies whether to show the loading/spinner box. Defaults to true.
  3619. // bodyOnly: Boolean?
  3620. // Specifies whether or not to cover the page header with an underlay element,
  3621. // as opposed to just covering the body area. Defaults to true.
  3622. this.loader = loader!==undefined? loader: true;
  3623. this.bodyOnly = bodyOnly!==undefined? bodyOnly: true;
  3624. }
  3625. =====*/
  3626. this.underlay = new (function(){
  3627. // summary:
  3628. // A singleton object to handle UI blocking for calls that should not
  3629. // allow user interaction.
  3630. var inc=0;
  3631. this.show = function(/* qd.app.underlay.__Args */kwArgs){
  3632. // summary:
  3633. // Show the underlay based on the passed kwArgs.
  3634. if(++inc){
  3635. var u1 = dojo.byId("topMoviesUnderlay"),
  3636. u2 = dojo.byId("queueUnderlay"),
  3637. args = dojo.mixin({loader:true, bodyOnly:true}, kwArgs||{});
  3638. if(u1){
  3639. dojo.style(u1, {display:"block", height:u1.parentNode.scrollHeight});
  3640. }
  3641. if(u2){
  3642. dojo.style(u2, {display:"block", height:u2.parentNode.scrollHeight});
  3643. }
  3644. if(!args.bodyOnly){
  3645. dojo.style("headerUnderlay", "display", "block");
  3646. }
  3647. if(args.loader){
  3648. var n = dojo.byId("loaderNode");
  3649. dojo.style(n, {display:"block", opacity:0});
  3650. dojo.fadeIn({node:n}).play();
  3651. }
  3652. }
  3653. };
  3654. this.hide = function(){
  3655. // summary:
  3656. // Hide the underlay.
  3657. if(!--inc){
  3658. var n = dojo.byId("loaderNode");
  3659. if(dojo.style(n, "display") == "block"){
  3660. var anim = dojo.fadeOut({node:n});
  3661. var __ac = dojo.connect(anim, "onEnd", function(){
  3662. dojo.disconnect(__ac);
  3663. dojo.style(n, "display", "none");
  3664. });
  3665. anim.play();
  3666. }
  3667. dojo.style("headerUnderlay", "display", "none");
  3668. dojo.style("topMoviesUnderlay", "display", "none");
  3669. dojo.style("queueUnderlay", "display", "none");
  3670. }
  3671. if(inc < 0){ inc=0; } // handle excessive calls to hide()
  3672. };
  3673. })();
  3674. this.loadingIcon = new (function(){
  3675. // summary:
  3676. // A singleton object that represents the loading icon at the top right.
  3677. var showing = false, timer;
  3678. this.__defineGetter__("showing", function(){
  3679. // summary:
  3680. // Returns whether or not the icon is currently visible.
  3681. return showing; // Boolean
  3682. });
  3683. this.show = function(){
  3684. // summary:
  3685. // Show this icon. If the error icon is visible, don't show it.
  3686. if(qd.app.errorIcon.showing){ return; }
  3687. if(showing){ return; }
  3688. dojo.query(".loadingIndicator, .bgLoadingSpinner").forEach(function(item){
  3689. dojo.style(item, {
  3690. opacity: 1,
  3691. display: "block"
  3692. });
  3693. });
  3694. showing = true;
  3695. // force it to go away eventually.
  3696. timer = setTimeout(dojo.hitch(this, function(){
  3697. this.hide();
  3698. }), 10000);
  3699. };
  3700. this.hide = function(){
  3701. // summary:
  3702. // Hide this icon.
  3703. dojo.query(".loadingIndicator, .bgLoadingSpinner").forEach(function(item){
  3704. item.style.display = "none";
  3705. });
  3706. showing = false;
  3707. if(timer){
  3708. clearTimeout(timer);
  3709. timer = null;
  3710. }
  3711. };
  3712. })();
  3713. this.errorIcon = new (function(){
  3714. // summary:
  3715. // A singleton object that controls the alert/error icon at the top right.
  3716. var showing = false;
  3717. this.__defineGetter__("showing", function(){
  3718. // summary:
  3719. // Returns whether or not the icon is currently visible.
  3720. return showing; // Boolean
  3721. });
  3722. this.show = function(){
  3723. // summary:
  3724. // Show the icon.
  3725. if(showing){ return; }
  3726. if(qd.app.loadingIcon.showing){
  3727. qd.app.loadingIcon.hide();
  3728. }
  3729. dojo.query(".loadingIndicator, .offlineIndicator").forEach(function(item){
  3730. dojo.style(item, {
  3731. opacity: 1,
  3732. display: "block"
  3733. });
  3734. });
  3735. showing = true;
  3736. };
  3737. this.hide = function(){
  3738. // summary:
  3739. // Hide the icon.
  3740. dojo.query(".loadingIndicator, .offlineIndicator").forEach(function(item){
  3741. item.style.display = "none";
  3742. });
  3743. showing = false;
  3744. };
  3745. })();
  3746. this.errorTooltip = new (function(){
  3747. // summary:
  3748. // A singleton object that controls the error tooltip, shown at the top right.
  3749. var fader, timeout, delay = 5000, duration = 1600, endHandle;
  3750. this.show = function(/* String */title, /* String */msg, /* Boolean? */persistIcon){
  3751. // summary:
  3752. // Show the indicator toolip with the given message parts.
  3753. // title: String
  3754. // The main message to show the user.
  3755. // msg: String
  3756. // The explanation for the user as to what happened.
  3757. // persistIcon: Boolean?
  3758. // Leave the icon showing if this is true. Defaults to false.
  3759. title = title || "An unknown error occured.";
  3760. msg = msg || "A unknown error occured with your last action.";
  3761. persistIcon = (persistIcon !== undefined) ? persistIcon : false;
  3762. var n = dojo.byId("indicatorTooltip");
  3763. if(timeout){
  3764. clearTimeout(timeout);
  3765. }
  3766. // stop the fader.
  3767. if(fader){
  3768. fader.stop();
  3769. fader = null;
  3770. }
  3771. // set the messages.
  3772. dojo.query("h1,p", n).forEach(function(node){
  3773. if(node.tagName.toLowerCase() == "h1"){
  3774. node.innerHTML = title;
  3775. } else {
  3776. node.innerHTML = msg;
  3777. }
  3778. });
  3779. // show the error icon.
  3780. qd.app.errorIcon.show();
  3781. // show the node.
  3782. dojo.style(n, {
  3783. opacity: 1,
  3784. display: "block"
  3785. });
  3786. // set up the fader
  3787. setTimeout(function(){
  3788. fader = dojo.fadeOut({ node: n, duration: duration });
  3789. endHandle = dojo.connect(fader, "onEnd", function(){
  3790. n.style.display = "none";
  3791. dojo.disconnect(endHandle);
  3792. endHandle = null;
  3793. if(!persistIcon){
  3794. setTimeout(function(){
  3795. qd.app.errorIcon.hide();
  3796. }, 1000);
  3797. }
  3798. });
  3799. fader.play();
  3800. }, delay);
  3801. };
  3802. this.hide = function(){
  3803. // summary:
  3804. // Force the tooltip to be hidden.
  3805. qd.app.errorIcon.hide();
  3806. var n = dojo.byId("indicatorTooltip");
  3807. if(timeout){
  3808. clearTimeout(timeout);
  3809. timeout = null;
  3810. }
  3811. if(fader){
  3812. fader.stop();
  3813. fader = null;
  3814. }
  3815. if(endHandle){
  3816. dojo.disconnect(endHandle);
  3817. endHandle = null;
  3818. }
  3819. n.style.display = "none";
  3820. };
  3821. })();
  3822. // deal with the online / offline indicators.
  3823. dojo.addOnLoad(function(){
  3824. dojo.connect(qd.services.network, "onChange", function(state){
  3825. if(state){
  3826. // we're online, hide the error stuff if needed.
  3827. qd.app.errorTooltip.hide();
  3828. } else {
  3829. qd.app.errorTooltip.show(
  3830. "Cannot reach the Netflix servers.",
  3831. "Ratings and Queue changes will by synced to Netflix when a connection can be re-established.",
  3832. true
  3833. );
  3834. }
  3835. });
  3836. });
  3837. this.switchPage = function(/* String */page){
  3838. // summary:
  3839. // Change to another top-level application page.
  3840. // page:
  3841. // "yourQueue", "topMovies", "auth", "preferences
  3842. var divId, menuId, bkClass;
  3843. switch(page){
  3844. case "yourQueue":
  3845. divId = "queueContentNode";
  3846. menuId = "bigNavYourQueue";
  3847. bkClass = false;
  3848. break;
  3849. case "topMovies":
  3850. divId = "topMoviesContainerNode";
  3851. menuId = "bigNavTopMovies";
  3852. bkClass = false;
  3853. break;
  3854. case "auth":
  3855. divId = "authContentNode";
  3856. menuId = "";
  3857. bkClass = true;
  3858. break;
  3859. case "preferences":
  3860. divId = "prefsContainerNode";
  3861. menuId = "";
  3862. bkClass = true;
  3863. break;
  3864. }
  3865. dijit.byId("contentNode").selectChild(divId);
  3866. qd.app.selectNav(menuId, "bigNav");
  3867. if(page == "topMovies"){
  3868. qd.app.topMovies.checkForRefresh();
  3869. }
  3870. // changes the background color of the app to
  3871. // more closely match the current page. Helps hide
  3872. // blemishes on window resize.
  3873. if(bkClass){
  3874. dojo.addClass(dojo.body(), "blueBk");
  3875. }else{
  3876. dojo.removeClass(dojo.body(), "blueBk");
  3877. }
  3878. };
  3879. this.selectNav = function(/* String */navItemId, /* String */navId){
  3880. // summary:
  3881. // Toggle selection styles for navigation items (just does
  3882. // the styling part; it doesn't actually set container node
  3883. // visibility or anything)
  3884. // navItemId:
  3885. // ID of the nav item to mark as selected
  3886. // navId:
  3887. // ID of the list in which the toggling is occurring
  3888. dojo.query("#"+navId+" li").removeClass("selected");
  3889. if (navItemId) {
  3890. dojo.addClass(dojo.byId(navItemId), "selected");
  3891. }
  3892. };
  3893. this.setTopRightNav = function(/* String */username){
  3894. // summary:
  3895. // Set up the navigation (username, prefs) on the top right of the screen.
  3896. if(username){
  3897. dojo.byId("topNavUser").innerHTML = "Welcome " + username;
  3898. dojo.byId("prefsUserName").innerHTML = username;
  3899. }
  3900. };
  3901. // single point of contact to determine when and/or whether some DnD is happening;
  3902. var _isDragging = false;
  3903. this.isDragging = function(){
  3904. // summary:
  3905. // Return whether or not something is being dragged.
  3906. return _isDragging; // Boolean
  3907. };
  3908. this.startDragging = function(){
  3909. // summary:
  3910. // Set the isDragging flag
  3911. _isDragging = true;
  3912. }
  3913. this.stopDragging = function(){
  3914. // summary:
  3915. // Unset the isDragging flag.
  3916. _isDragging = false;
  3917. }
  3918. // setup the dragging topics
  3919. dojo.subscribe("/dnd/start", this, "startDragging");
  3920. dojo.subscribe("/dnd/cancel", this, "stopDragging");
  3921. dojo.subscribe("/dnd/drop", this, "stopDragging");
  3922. // set up the application-level behaviors
  3923. dojo.behavior.add({
  3924. // Top-level navigation
  3925. "#bigNavTopMovies a": {
  3926. onclick:dojo.hitch(this, function(){
  3927. this.switchPage("topMovies");
  3928. return false;
  3929. })
  3930. },
  3931. // Top-level navigation
  3932. "#bigNavYourQueue a": {
  3933. onclick:dojo.hitch(this, function(){
  3934. this.switchPage("yourQueue");
  3935. return false;
  3936. })
  3937. }
  3938. });
  3939. })();
  3940. }
  3941. if(!dojo._hasResource["qd.app.queueList"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  3942. dojo._hasResource["qd.app.queueList"] = true;
  3943. dojo.provide("qd.app.queueList");
  3944. (function(){
  3945. var REMOVAL_CONFIRMATION_TIMER_DURATION = 5000; // milliseconds to show the keep/remove button
  3946. var create = function(tagName, text, _class, parent){
  3947. // summary:
  3948. // Development helper to create nodes.
  3949. var node = dojo.doc.createElement(tagName);
  3950. if (text) { node.innerHTML = text };
  3951. if (_class) { node.className = _class };
  3952. if (tagName =="a") { node.href = "#" };
  3953. if (parent) { parent.appendChild(node) };
  3954. return node;
  3955. }
  3956. var _reg = new RegExp(/\$\{.*?\}/gi);
  3957. var makeTemplate = function(/* String */str, /* Object */item){
  3958. // summary:
  3959. // Build a queue DOM template from the given string, using
  3960. // simple ${variable} substitution.
  3961. // item:
  3962. // Netflix item to use for template variables, which should be
  3963. // named properties on the object.
  3964. var tpl = "";
  3965. var props = [];
  3966. dojo.forEach(str.match(_reg), function(p){
  3967. props.push(dojo.trim(p.substring(2,p.length-1)));
  3968. });
  3969. var frags = str.split(_reg);
  3970. for(var i=0;i<frags.length;i++){
  3971. tpl += frags[i];
  3972. if(i<frags.length-1){
  3973. tpl += item[props[i]] || dojo.getObject(props[i], false, item);
  3974. }
  3975. }
  3976. return tpl;
  3977. }
  3978. var tpls = {
  3979. queue: '<tr class="listQueuedRow" movie="${guid}">'
  3980. + '<td class="index dojoDndHandle"><div><input dojoAttachPoint="textbox" dojoAttachEvent="keyup:onTextKeyUp,blur:onTextBlur" type="text" value="" /></div></td>'
  3981. + '<td class="top dojoDndHandle" dojoAttachEvent="click:onTopClick"></td>'
  3982. + '<td class="title dojoDndHandle" dojoAttachEvent="click:onTitleClick">${title.title}</td>'
  3983. + '<td class="instant">${instantStr}</td>'
  3984. + '<td class="stars dojoDndHandle"><span class="starRating${starRatingEnabledStr}" style="visibility:hidden;" dojoAttachPoint="ratingNode"></span></td>'
  3985. + '<td class="genre dojoDndHandle">${genreStr}</td>'
  3986. + '<td class="format dojoDndHandle">${format}</td>'
  3987. + '<td class="remove"><div>'
  3988. + '<span dojoAttachPoint="removeButtonNode" class="button" dojoAttachEvent="click:onRemClick"></span>'
  3989. + '<span dojoAttachPoint="confirmButtonNode" class="confirm">'
  3990. + '<span class="confirmRemove" dojoAttachEvent="click:onConfirmRemoveClick"></span>'
  3991. + '<span class="keep" dojoAttachEvent="click:onKeepClick"></span>'
  3992. + '</span>'
  3993. + '</div></td>'
  3994. + '</tr>',
  3995. at_home: '<tr class="listQueuedRow" id="${guid}" movie="${guid}">'
  3996. + '<td class="title" dojoAttachEvent="click:onTitleClick, mouseover:onTitleOver, mouseout:onTitleOut">${title.title}</td>'
  3997. + '<td class="instant">${instantStr}</td>'
  3998. + '<td class="stars dojoDndHandle"><span class="starRating${starRatingEnabledStr}" style="visibility:hidden;" dojoAttachPoint="ratingNode"></span></td>'
  3999. + '<td class="shipped">${shipped}</td>'
  4000. + '<td class="arrive">${estimatedArrival}</td>'
  4001. + '<td class="problem"><a href="#" dojoAttachEvent="click:onProblemClick">Report a problem</a></td>'
  4002. + '</tr>',
  4003. /// hmmm.. This is the same as 'queue'. May need to change how we fetch templates - with a function
  4004. instant: '<tr class="listQueuedRow" movie="${guid}">'
  4005. + '<td class="index dojoDndHandle"><div><input dojoAttachPoint="textbox" dojoAttachEvent="keyup:onTextKeyUp,blur:onTextBlur" type="text" value="" /></div></td>'
  4006. + '<td class="top dojoDndHandle" dojoAttachEvent="click:onTopClick"></td>'
  4007. + '<td class="title dojoDndHandle" dojoAttachEvent="click:onTitleClick">${title.title}</td>'
  4008. + '<td class="instant">${instantStr}</td>'
  4009. + '<td class="stars dojoDndHandle"><span class="starRating${starRatingEnabledStr}" style="visibility:hidden;" dojoAttachPoint="ratingNode"></span></td>'
  4010. + '<td class="genre dojoDndHandle">${genreStr}</td>'
  4011. + '<td class="remove"><div>'
  4012. + '<span dojoAttachPoint="removeButtonNode" class="button" dojoAttachEvent="click:onRemClick"></span>'
  4013. + '<span dojoAttachPoint="confirmButtonNode" class="confirm">'
  4014. + '<span class="confirmRemove" dojoAttachEvent="click:onConfirmRemoveClick"></span>'
  4015. + '<span class="keep" dojoAttachEvent="click:onKeepClick"></span>'
  4016. + '</span>'
  4017. + '</div></td>'
  4018. + '</tr>',
  4019. watched: '<tr class="listQueuedRow" movie="${guid}">'
  4020. +'<td class="viewed">${watched}</td>'
  4021. + '<td class="title" dojoAttachEvent="click:onTitleClick">${title.title}</td>'
  4022. + '<td class="instant">${instantStr}</td>'
  4023. + '<td class="stars dojoDndHandle"><span class="starRating${starRatingEnabledStr}" style="visibility:hidden;" dojoAttachPoint="ratingNode"></span></td>'
  4024. + '<td class="genre">&nbsp;</td>'
  4025. + '<td class="details">&nbsp;</td>'
  4026. + '</tr>',
  4027. history: '<tr class="listQueuedRow" movie="${guid}">'
  4028. + '<td class="title" dojoAttachEvent="click:onTitleClick">${title.title}</td>'
  4029. + '<td class="stars dojoDndHandle"><span class="starRating${starRatingEnabledStr}" style="visibility:hidden;" dojoAttachPoint="ratingNode"></span></td>'
  4030. + '<td class="shipped">${shipped}</td>'
  4031. + '<td class="returned">${returnedStr}</td>'
  4032. + '<td class="details">${detailsStr}</td>'
  4033. + '</tr>',
  4034. saved: '<tr class="listQueuedRow" movie="${guid}">'
  4035. + '<td class="title" dojoAttachEvent="click:onTitleClick">${title.title}</td>'
  4036. + '<td class="stars dojoDndHandle"><span class="starRating${starRatingEnabledStr}" style="visibility:hidden;" dojoAttachPoint="ratingNode"></span></td>'
  4037. + '<td class="genre">${genreStr}</td>'
  4038. + '<td class="availability">${title.dates.availability}</td>'
  4039. + '<td class="remove"><div>'
  4040. + '<span dojoAttachPoint="removeButtonNode" class="button" dojoAttachEvent="click:onRemClick"></span>'
  4041. + '<span dojoAttachPoint="confirmButtonNode" class="confirm">'
  4042. + '<span class="confirmRemove" dojoAttachEvent="click:onConfirmRemoveClick"></span>'
  4043. + '<span class="keep" dojoAttachEvent="click:onKeepClick"></span>'
  4044. + '</span>'
  4045. + '</div></td>'
  4046. + '</tr>',
  4047. noItems: '<tr class="listQueuedRow noItems">'
  4048. + '<td colspan="${colspan}">There are no items in this list.</td>'
  4049. + '</tr>'
  4050. };
  4051. qd.app.nonItem = function(){
  4052. // summary:
  4053. // Initialize a queue to show the "No items found" display rather
  4054. // than a table full of items.
  4055. this.item = {};
  4056. this.constructor = function(options, parentNode){
  4057. dojo.mixin(this, options);
  4058. this.item.colspan = (this.type=="queue"||this.type=="instant") ? "8" : "6";
  4059. this.type = "noItems";
  4060. var tpl = makeTemplate(tpls[this.type], this.item);
  4061. var div = dojo.doc.createElement("div");
  4062. div.innerHTML = tpl;
  4063. this.domNode = div.firstChild;
  4064. parentNode.appendChild(this.domNode);
  4065. };
  4066. this.destroy = function(){
  4067. dojo._destroyElement(this.domNode);
  4068. delete this;
  4069. };
  4070. this.constructor.apply(this, arguments);
  4071. };
  4072. qd.app.queueItem = function(){
  4073. this.id = "";
  4074. this.item = {};
  4075. this._connects = [];
  4076. this.type = "";
  4077. this.parent = null;
  4078. this.resetFormat = "";
  4079. this.__defineGetter__("position", function() {
  4080. return this.item.position;
  4081. });
  4082. this.__defineSetter__("position", function(pos) {
  4083. if (this.textbox) {
  4084. this.item.position = pos;
  4085. this.textbox.value = pos;
  4086. }
  4087. });
  4088. this.constructor = function(/* Object */options, /* Node */parentNode){
  4089. // summary:
  4090. // Build up a queue item (one row in the display), instantiate
  4091. // the item in the DOM, etc.
  4092. // options:
  4093. // Property bag to use as a mixin on this queue item.
  4094. // parentNode:
  4095. // Node in which to insert this queue item into the DOM.
  4096. dojo.mixin(this, options);
  4097. this.id = this.item.id;
  4098. this.guid = this.item.guid;
  4099. this.item.genreStr = this.item.title.categories[0];
  4100. // play button
  4101. this.item.instantStr = (
  4102. this.type=="instant"
  4103. || this.type=="watched"
  4104. || ("instant"||"Instant") in this.item.title.formats
  4105. || this.item.format=="Instant"
  4106. || this.type=="watchedList") ? '<a href="#" dojoAttachEvent="click:onPlayClick">PLAY</a>' : '&nbsp';
  4107. if(this.item.title.dates){
  4108. this.item.estimatedArrival = this.item.estimatedArrival || "";
  4109. this.item.shipped = this.item.shipped || "Shipping Now";
  4110. }
  4111. this.item.detailsStr = this.item.returned ? '&nbsp;' : '<a href="#" dojoAttachEvent="click:onProblemClick">Report a problem</a>';
  4112. this.item.returnedStr = this.item.returned || '--';
  4113. this.item.title.dates.availability = this.item.title.dates.availability || "--";
  4114. if(this.item.guid) {
  4115. this.item.starRatingEnabledStr = (!this.item.title.guid.match(/titles\/discs\//) || this.item.title.title.match(/Disc 1$/)) ? " enabled" : " nonFirst";
  4116. }
  4117. //this.item.starRatingEnabledStr
  4118. var tpl = makeTemplate(tpls[this.type], this.item);
  4119. var div = dojo.doc.createElement("div");
  4120. div.innerHTML = tpl;
  4121. var node = div.firstChild;
  4122. parentNode.appendChild(node);
  4123. this.attachEvents(node);
  4124. this.postDom();
  4125. };
  4126. this.postDom = function(){
  4127. // summary:
  4128. // Trigger function to execute after the queue item's DOM
  4129. // representation is added to the page. Sets up the textbox
  4130. // containing the item's position and registers the key
  4131. // handlers for it.
  4132. if (this.textbox) {
  4133. this.textbox.value = this.position;
  4134. this.textbox.maxLength = 3;
  4135. this._connects.push(dojo.connect(this.textbox, "keypress", this, function(evt){
  4136. if(evt.keyIdentifier){
  4137. return true;
  4138. }
  4139. var k = evt.keyCode;
  4140. if (k > 31 && (k < 48 || k > 57)) {
  4141. dojo.stopEvent(evt);
  4142. return false;
  4143. }
  4144. return true;
  4145. }));
  4146. }
  4147. };
  4148. this.setRatingData = function(/* Number */user, /* Number */pred, /* Number */avg){
  4149. // summary:
  4150. // Create the star rating widget for this queue item.
  4151. // user:
  4152. // The user rating value (1-5, "not_interested", "no_opinion").
  4153. // pred:
  4154. // The predicted rating value (1-5, "not_interested", "no_opinion").
  4155. // avg:
  4156. // The member average rating value (1-5, "not_interested", "no_opinion").
  4157. var node=this.ratingNode, type="average", rating=0;
  4158. if(node) {
  4159. if(user > 0){
  4160. rating = user;
  4161. type = "user";
  4162. }else if(pred > 0){
  4163. rating = pred;
  4164. type = "predicted";
  4165. }else if(avg > 0){
  4166. rating = avg;
  4167. type = "average";
  4168. }
  4169. qd.app.ratings.buildRatingWidget(node, type, rating);
  4170. dojo.style(this.ratingNode, "visibility", "visible");
  4171. }
  4172. };
  4173. this.attachEvents = function(/* Node */node){
  4174. // summary:
  4175. // Register event connections defined in the queue item's template.
  4176. // node:
  4177. // The item's DOM node.
  4178. this.domNode = node;
  4179. var nodes = this.domNode.getElementsByTagName("*");
  4180. dojo.forEach(nodes, function(n){
  4181. var att = n.getAttribute("dojoAttachEvent");
  4182. if (att) {
  4183. dojo.forEach(att.split(","), function(pr){
  4184. this._connects.push(dojo.connect(n, pr.split(":")[0], this, pr.split(":")[1]));
  4185. }, this);
  4186. }
  4187. var att = n.getAttribute("dojoAttachPoint");
  4188. if (att) {
  4189. this[att] = n;
  4190. }
  4191. }, this);
  4192. };
  4193. this.destroy = function(){
  4194. // summary: Tear down the queue item.
  4195. dojo.forEach(this._connects, dojo.disconnect, dojo);
  4196. dojo._destroyElement(this.domNode);
  4197. delete this;
  4198. };
  4199. this.update = function(/* Object */newItem){
  4200. // summary:
  4201. // New item returned from server (usually just the new position).
  4202. this.item = newItem;
  4203. };
  4204. this.reset = function(){
  4205. // summary:
  4206. // If the server call is unsuccessful, do any resetting here.
  4207. this.textbox.value = this.position;
  4208. if(this.resetFormat){
  4209. setTimeout(dojo.hitch(this, function(){
  4210. this.item.preferredFormatStr = this.resetFormat;
  4211. this.resetFormat = "";
  4212. this.selFormat.value =this.selFormat.selected = this.item.preferredFormatStr;
  4213. }), 500)
  4214. }
  4215. };
  4216. function toggleRemoveButtonState(/* Node */outNode, /* Node */inNode, /* Number? */duration){
  4217. // summary:
  4218. // Animated toggle to switch back and forth between the
  4219. // "(-)" (delete) and "Remove | Keep" queue item buttons.
  4220. // outNode:
  4221. // Button node to fade out.
  4222. // inNode:
  4223. // Button node to fade in.
  4224. // duration:
  4225. // Optional duration in milliseconds. Default 200.
  4226. dojo.style(inNode, {display:"inline-block",opacity:0});
  4227. var anim = dojo.fx.combine([
  4228. dojo.fadeOut({node:outNode, duration:duration || 200}),
  4229. dojo.fadeIn({node:inNode, duration:duration || 200})
  4230. ]),
  4231. __h = dojo.connect(anim, "onEnd", function(){
  4232. dojo.disconnect(__h);
  4233. dojo.style(outNode, "display", "none");
  4234. });
  4235. anim.play();
  4236. }
  4237. this.cancelRemoveButtonTimer = function(){
  4238. // summary:
  4239. // Turn off the Remove button timer, which resets the
  4240. // "Remove | Keep" button back to "(-)" (delete) after
  4241. // a few seconds of inactivity.
  4242. clearTimeout(this.confirmationTimer);
  4243. this.confirmationTimer = null;
  4244. }
  4245. this.onFormatChange = function(evt){
  4246. // summary:
  4247. // Handle resetting the item's format.
  4248. if(evt.target.value!=this.item.preferredFormatStr){
  4249. this.resetFormat = this.item.preferredFormatStr;
  4250. this.item.preferredFormatStr=evt.target.value;
  4251. this.parent.changeFormat(this);
  4252. }
  4253. };
  4254. this.onRemClick = function(){
  4255. // summary: Show the "Remove | Keep" button for a few seconds.
  4256. toggleRemoveButtonState(this.removeButtonNode, this.confirmButtonNode);
  4257. // FIXME: this if() shouldn't be necessary, but since this method gets called
  4258. // twice per click, this ensures that the toggle only happens once
  4259. if(!this.confirmationTimer){
  4260. this.confirmationTimer = setTimeout(
  4261. dojo.hitch(this, function(){
  4262. toggleRemoveButtonState(this.confirmButtonNode, this.removeButtonNode, 500);
  4263. }),
  4264. REMOVAL_CONFIRMATION_TIMER_DURATION
  4265. );
  4266. }
  4267. };
  4268. this.onKeepClick = function(){
  4269. // summary: Cancel the item deletion process; keep the item.
  4270. this.cancelRemoveButtonTimer();
  4271. toggleRemoveButtonState(this.confirmButtonNode, this.removeButtonNode);
  4272. };
  4273. this.onConfirmRemoveClick = function(){
  4274. // summary: Remove an item from the queue.
  4275. this.cancelRemoveButtonTimer();
  4276. this.parent.remove(this);
  4277. };
  4278. this.onProblemClick = function(){
  4279. // summmary: Show the netflix.com page for reporting problems with discs.
  4280. air.navigateToURL(new air.URLRequest("https://www.netflix.com/DiscProblems"));
  4281. };
  4282. this.onPlayClick = function(){
  4283. // summary: Development helper to try and play Instant Watch titles in AIR.
  4284. // test: try to open up a new AIR window using the url stuff from Netflix's api
  4285. var href = "http://www.netflix.com/CommunityAPIPlay?devKey="
  4286. + qd.app.authorization.consumer.key
  4287. + "&movieid="
  4288. + encodeURIComponent(this.item.guid)
  4289. + "&nbb=y";
  4290. console.log("PLAY: " + href);
  4291. air.navigateToURL(new air.URLRequest(href));
  4292. };
  4293. this.onTitleClick = function(){
  4294. // summary: Show the movie info detail dialog for this item.
  4295. qd.app.movies.showInfo(this.item.guid);
  4296. };
  4297. this.onTitleOver = function(){};
  4298. this.onTitleOut = function(){};
  4299. this.onTopClick = function(){
  4300. // summary: Move this queue item to the top of the queue (position 1)
  4301. if(this.position>1){
  4302. this.parent.reorder(this, 1);
  4303. }
  4304. };
  4305. this.onTextKeyUp = function(evt){
  4306. // summary:
  4307. // Monitor key events in the item's position textbox, and on
  4308. // Enter, reorder the queue accordingly.
  4309. var k = evt.keyCode;
  4310. if(k == 13){
  4311. this.parent.reorder(this, this.textbox.value);
  4312. }
  4313. };
  4314. this.onTextBlur = function(evt){
  4315. // summary:
  4316. // Reorder the queue if the user changed the value of the
  4317. // item's position textbox.
  4318. if(this.textbox.value != this.position){
  4319. this.textbox.value = this.position;
  4320. }
  4321. };
  4322. this.constructor.apply(this, arguments);
  4323. }
  4324. qd.app.queueList = function(){
  4325. this.recentWatchedAmount = 5;
  4326. this.type = "";
  4327. this.result = {};
  4328. this.list = [];
  4329. this.domNode = null;
  4330. this.registry = {};
  4331. this.dndSource = null;
  4332. this.constructor = function(/* Object */options, /* Node */parentNode){
  4333. // summary:
  4334. // Initialize a queue by setting up drag and drop, etc.
  4335. // options:
  4336. // Property bag to use as a mixin on this queue.
  4337. // parentNode:
  4338. // Node in which to insert this queue item into the DOM.
  4339. dojo.mixin(this, options);
  4340. if(this.type=="watched"){
  4341. this.result.items = dojo.filter(this.result.items, function(m, i){ if(i<this.recentWatchedAmount){ return m; }}, this);
  4342. }
  4343. this.domNode = dojo.byId(parentNode);
  4344. this.dndSource = new dojo.dnd.Source(this.domNode, {
  4345. accept: options.type || "movie",
  4346. skipForm: true,
  4347. withHandles: true,
  4348. isSource: options.canDrag || false,
  4349. singular: true,
  4350. creator: dojo.hitch(this, this.createItem)
  4351. });
  4352. if(options.canDrag){
  4353. dojo.connect(this.dndSource, "onDropInternal", this, "onDrop");
  4354. dojo.connect(this.dndSource, "onDndCancel", this, "onDragCancel");
  4355. }
  4356. this.dndSource._legalMouseDown = function(e){
  4357. // hack! only allow left-click dragging
  4358. if(e.button){ return false; }
  4359. return dojo.dnd.Source.prototype._legalMouseDown.call(this, e);
  4360. };
  4361. this.dndSource.insertNodes(false, dojo.filter(this.result.items, function(i){
  4362. // in English: for "queue" and "instant", skip when position==null
  4363. return i.position || (this.type!="queue" && this.type!="instant");
  4364. }, this));
  4365. this.noItemCheck();
  4366. this.onLoad();
  4367. };
  4368. this.onLoad = function(){
  4369. // summary: Hook for code to run when the queue's data is loaded.
  4370. setTimeout(dojo.hitch(qd.app.queue, "onLoad", this), 100);
  4371. };
  4372. this.onChange = function(/*String*/typeOfChange){
  4373. // summary: Hook for code to run when the queue's data changes.
  4374. qd.app.queue.onChange(this, typeOfChange);
  4375. };
  4376. this.inQueue = function(/* String */movieId){
  4377. // summary:
  4378. // Check whether an item is in this queue.
  4379. // movieId:
  4380. // Netflix item guid to check.
  4381. var b = dojo.some(this.list, function(listItem){
  4382. var guid = listItem.item.title.guid;
  4383. return (guid == movieId);
  4384. }, this);
  4385. return b; // Boolean
  4386. };
  4387. this.inQueueByTerm = function(/* String */term){
  4388. // summary:
  4389. // Check whether an item is in this queue, by item title.
  4390. // movieId:
  4391. // Netflix item title to check.
  4392. return dojo.some(this.list, function(listItem){
  4393. return listItem.item.title.title == term;
  4394. }, this);
  4395. };
  4396. this.noItemCheck = function(){
  4397. // summary:
  4398. // Check to see if this queue is empty, and if so,
  4399. // initialize the "no item" display.
  4400. if ((this.list.length == 0 && !this.noResultItem) || (this.list.length == 1 && !this.list[0])) {
  4401. this.noResultItem = new qd.app.nonItem({
  4402. type: this.type
  4403. }, this.domNode);
  4404. }else if(this.noResultItem){
  4405. this.noResultItem.destroy();
  4406. this.noResultItem = null;
  4407. }
  4408. };
  4409. this.destroy = function(){
  4410. // summary: Tear down this queue object.
  4411. this.dndSource.destroy();
  4412. dojo.forEach(this.list, function(m){
  4413. m.destroy();
  4414. });
  4415. if (this.noResultItem) {
  4416. this.noResultItem.destroy();
  4417. }
  4418. delete this;
  4419. // concerned that a user will logout in the middle
  4420. // of a server call, which may cause a temporary
  4421. // closure.
  4422. this.destroyed = true;
  4423. };
  4424. this.createItem = function(/* Object */item, /* String? */hint){
  4425. // summary:
  4426. // Turns the movie item provided into an object the
  4427. // DnD system can handle, or creates an avatar for a
  4428. // dragged item.
  4429. // item:
  4430. // A movie object on create, and a listItem on drag.
  4431. // hint:
  4432. // When set to "avatar", indicates that we only need
  4433. // to create a drag avatar; otherwise, go ahead and
  4434. // create a full queueItem object.
  4435. if(hint == "avatar"){
  4436. // creating an avatar; just build a simple node
  4437. var node = dojo.doc.createElement("div");
  4438. node.className = "movieDragAvatar";
  4439. node.innerHTML = item.item.title.title;
  4440. // ref to orginial node. Having trouble getting it
  4441. // onCancel without this ref.
  4442. this.draggingitem = item;
  4443. dojo.style(this.draggingitem.domNode, "visibility", "hidden");
  4444. dojo.style(this.draggingitem.ratingNode, "visibility", "hidden");
  4445. return {
  4446. node: node,
  4447. data: item,
  4448. type: this.type
  4449. }
  4450. }
  4451. // creating a full item; instantiate a queueItem
  4452. var listItem = new qd.app.queueItem({
  4453. item: item,
  4454. type: this.type,
  4455. parent: this
  4456. }, this.domNode);
  4457. this.list.push(listItem);
  4458. this.registry[item.id] = listItem;
  4459. return {
  4460. node: listItem.domNode,
  4461. data: listItem,
  4462. type: this.type
  4463. }
  4464. };
  4465. this.onDrop = function(/* Node[] */nodes){
  4466. // summary:
  4467. // Handle drop events coming from the DnD system; typically
  4468. // this means reordering the queue.
  4469. // nodes:
  4470. // An array of the DOM nodes selected and being dropped; we
  4471. // currently only allow single-selection, so this is hard-coded
  4472. // to only ever look at the first node.
  4473. var node = nodes[0];
  4474. dojo.style(this.draggingitem.domNode, "visibility", "visible");
  4475. dojo.style(this.draggingitem.ratingNode, "visibility", "visible");
  4476. this.dndSource.getAllNodes().forEach(function(n, i){
  4477. var listItem = this.dndSource.getItem(n.id).data;
  4478. if(n.id == node.id){
  4479. this.reorder(listItem, i+1, false);
  4480. // save the declared style (dojo.style gets computed style)
  4481. var col = n.style.backgroundColor;
  4482. dojo.style(n, "background-color", "#fff");
  4483. var anim = dojox.fx.highlight({
  4484. node: n,
  4485. duration: 1500,
  4486. easing: dojo.fx.easing.cubicIn
  4487. });
  4488. var __h = dojo.connect(anim, "onEnd", function(){
  4489. dojo.disconnect(__h);
  4490. dojo.style(n, "background-color", col);
  4491. });
  4492. anim.play();
  4493. }
  4494. listItem.position = i+1;
  4495. }, this);
  4496. this.draggingitem = null;
  4497. };
  4498. this.onDragCancel = function(){
  4499. // summary: Clean up after an aborted drag operation.
  4500. if(this.draggingitem){
  4501. dojo.style(this.draggingitem.domNode, "visibility", "visible");
  4502. dojo.style(this.draggingitem.ratingNode, "visibility", "visible");
  4503. this.draggingitem = null;
  4504. }
  4505. };
  4506. this.highlight = function(/* String */movieId){
  4507. // summary:
  4508. // Run an animation to briefly highlight a dropped queue item.
  4509. // movieId:
  4510. // Netflix item guid to highlight.
  4511. var listItem = (movieId.indexOf("catalog")>-1) ? this.byTitle(movieId): this.byId(movieId);
  4512. if(listItem){
  4513. dijit.scrollIntoView(listItem.domNode);
  4514. var anim = dojo.animateProperty({
  4515. node: listItem.domNode,
  4516. duration: 1000,
  4517. properties: { backgroundColor: { start: "#ffff00", end: "#ffffff" } },
  4518. onEnd: function(){
  4519. dojo.style(listItem.domNode, "backgroundColor", "transparent");
  4520. }
  4521. }).play();
  4522. }
  4523. };
  4524. this.addMovie = function(/* Object */item){
  4525. // summary:
  4526. // Adds movie to the list.
  4527. // description:
  4528. // Called from qd.app.queue
  4529. // note:
  4530. // The API allows for item to be added
  4531. // a certain position. Currently not
  4532. // supported by our UI.
  4533. var options = {
  4534. guid: item.guid,
  4535. title: item.title,
  4536. //format:"DVD",
  4537. url: (this.type == "instant") ? "queues/instant" : "queues/disc",
  4538. };
  4539. qd.app.loadingIcon.show();
  4540. var def = qd.service.queues.modify(options);
  4541. def.addCallback(this, function(data){
  4542. qd.app.loadingIcon.hide();
  4543. if(this.destroyed){ delete this; return; }
  4544. var item = data.created[0];
  4545. qd.services.item(item);
  4546. this.noItemCheck();
  4547. var listItem = this.dndSource.insertNodes(false, [item]);
  4548. dojo.forEach(this.list, function(m, i){
  4549. m.position = i + 1;
  4550. });
  4551. //this.highlight(listItem.id);
  4552. qd.app.queue.getRatings([item], dojo.hitch(this, function(ratings){
  4553. this.setRatingData(ratings);
  4554. }));
  4555. this.onChange("add");
  4556. });
  4557. def.addErrback(this, function(err){
  4558. qd.app.loadingIcon.hide();
  4559. console.warn("Error on ADD MOVIE status:", err.status.results.message);
  4560. });
  4561. };
  4562. this.setRatingData = function(/* Object */ratings){
  4563. // summary:
  4564. // For each passed ratings chunk, find the list item through
  4565. // the title it represents, and set the ratings data.
  4566. dojo.forEach(ratings, function(m){
  4567. var title = this.byTitle(m.guid);
  4568. if(title){
  4569. title.setRatingData(m.ratings.user, m.ratings.predicted, m.ratings.average);
  4570. }
  4571. }, this);
  4572. qd.app.ratings.activateRatingWidgets();
  4573. };
  4574. this.remove = function(/* Object */listItem){
  4575. // summary:
  4576. // Triggered by the delete icon in an item
  4577. var url=(this.type == "queue")?"queues/disc/available":(this.type=="instant")?"queues/instant/available":"queues/disc/saved";
  4578. var movieId = listItem.id;
  4579. qd.app.loadingIcon.show();
  4580. qd.service.queues.remove({
  4581. url: url,
  4582. guid: listItem.guid.substring(listItem.guid.lastIndexOf("/")+1, listItem.guid.length),
  4583. title: listItem.item.title.title
  4584. }).addCallback(this, function(res){
  4585. qd.app.loadingIcon.hide();
  4586. if(this.destroyed){
  4587. delete this;
  4588. return;
  4589. }
  4590. this.removeDisplayItem(listItem);
  4591. qd.app.movies.queueMovieChange(movieId, this.type);
  4592. // needs to be done after anim -> this.noItemCheck();
  4593. }).addErrback(this, function(err){
  4594. qd.app.loadingIcon.hide();
  4595. if(this.destroyed){
  4596. delete this;
  4597. return;
  4598. }
  4599. console.warn("Error on remove status:", err.status.results.message);
  4600. //listItem.reset();
  4601. });
  4602. };
  4603. this.removeDisplayItem = function(/* Object*/listItem){
  4604. // summary:
  4605. // The visual, animated part of removing an item
  4606. dojo.style(listItem.domNode, "backgroundColor", "#ff0000");
  4607. dojo.fadeOut({node:listItem.domNode, onEnd:dojo.hitch(this, function(){
  4608. var a = dojo.fx.wipeOut({
  4609. node: listItem.domNode,
  4610. duration: 500,
  4611. onEnd: dojo.hitch(this, function(){
  4612. this._removeListItem(listItem);
  4613. })
  4614. }).play();
  4615. })}).play();
  4616. };
  4617. this._removeListItem = function(/* Object */listItem){
  4618. // summary:
  4619. // After server call and animaton, finally
  4620. // remove domNode and reorder list.
  4621. var i = this.getIndex(listItem.id);
  4622. this.dndSource.delItem(listItem.domNode.id);
  4623. listItem.destroy();
  4624. this.list.splice(i, 1);
  4625. dojo.forEach(this.list, function(m, i){
  4626. m.position = i+1;
  4627. });
  4628. this.noItemCheck();
  4629. this.onChange("remove");
  4630. };
  4631. this.renumber = function(/* Object*/listItem){
  4632. // summary: Renumber (internally; don't update the display) the queue's items.
  4633. // note: Be careful; position is one-based, while index is zero-based.
  4634. var i = this.getIndex(listItem.id);
  4635. this.list.splice(i, 1);
  4636. this.list.splice(listItem.position - 1, 0, listItem);
  4637. dojo.forEach(this.list, function(m, i){
  4638. m.position = i + 1;
  4639. });
  4640. };
  4641. this.reorder = function(/* Object */listItem, /* Number */pos, /* Boolean? */animate){
  4642. // summary:
  4643. // Reorder the queue after a position textbox change or a drag
  4644. // and drop operation (but not clicking the "top" button).
  4645. // listItem:
  4646. // Queue list item just changed.
  4647. // pos:
  4648. // Item's new position.
  4649. // animate:
  4650. // Optional flag (default true) to indicate to animate the change.
  4651. if(typeof animate == "undefined"){ animate = true; }
  4652. var options = {
  4653. guid:listItem.item.guid,
  4654. position:pos,
  4655. title: listItem.item.title.title,
  4656. url:(this.type == "queue")?"queues/disc":"queues/instant"
  4657. };
  4658. qd.app.loadingIcon.show();
  4659. qd.service.queues.modify(options).addCallback(this, function(res){
  4660. qd.app.loadingIcon.hide();
  4661. if(this.destroyed){ delete this; return; }
  4662. if(res.code=="201" || res.code=="200"){
  4663. listItem.update(res.created[0]);
  4664. qd.services.item(res.created[0]);
  4665. if(animate){
  4666. this.reorderDisplay(listItem);
  4667. }
  4668. this.onChange("reorder");
  4669. }else{
  4670. console.warn("reorder, bad status:", res)
  4671. }
  4672. }).addErrback(this, function(err){
  4673. qd.app.loadingIcon.hide();
  4674. console.warn("Error on reorder status:", err.status.results.message);
  4675. listItem.reset();
  4676. });
  4677. };
  4678. this.reorderDisplay = function(/* Object*/listItem){
  4679. // summary:
  4680. // Run an animation to show the queue order changing.
  4681. this.renumber(listItem);
  4682. // animate old row to red
  4683. // move old row to new position
  4684. // animate new row from yellow to white to transparent
  4685. var self = this;
  4686. dojo.animateProperty({
  4687. node: listItem.domNode,
  4688. duration: 500,
  4689. properties: { backgroundColor: { start: "#ffffff", end: "#ff0000" }, },
  4690. onEnd:function(){
  4691. var refNode = listItem.domNode.parentNode;
  4692. var node = listItem.domNode.parentNode.removeChild(listItem.domNode);
  4693. dojo.place(node, refNode, listItem.position-1 );
  4694. self.highlight(listItem.id);
  4695. }
  4696. }).play();
  4697. };
  4698. this.attachEvents = function(){
  4699. // summary:
  4700. // Register event connections defined in the queue's template.
  4701. // node:
  4702. // The queue's DOM node.
  4703. dojo.query(".listQueuedRow", this.domNode).forEach(function(node, i){
  4704. this.list[i].attachEvents(node);
  4705. this.list[i].postDom();
  4706. }, this);
  4707. };
  4708. this.byId = function(/* String */id){
  4709. // summary:
  4710. // Return the list item based on the queue item id.
  4711. return this.registry[id]; // Object
  4712. };
  4713. this.byTitle = function(/* String */guid){
  4714. // summary:
  4715. // Return the list item based on the guid of the title it represents.
  4716. for(var i=0, l=this.list.length, li; i<l; i++){
  4717. li = this.list[i];
  4718. if(li.item.title.guid == guid){
  4719. return li;
  4720. }
  4721. }
  4722. return null;
  4723. };
  4724. this.byTerm = function(/* String */term){
  4725. // summary:
  4726. // Return the list item based on the string title it represents.
  4727. for(var i=0, l=this.list.length, li; i<l; i++){
  4728. li = this.list[i];
  4729. if(li.item.title.title == term){
  4730. return li;
  4731. }
  4732. }
  4733. return null;
  4734. };
  4735. this.getIndex = function(/* String */id){
  4736. // summary:
  4737. // Return the internal array index of the list item having the given ID.
  4738. var index = -1;
  4739. dojo.some(this.list, function(m, i){
  4740. if(m.id==id){ index=i; return true;}
  4741. }, this);
  4742. return index; // Number
  4743. };
  4744. this.constructor.apply(this, arguments)
  4745. };
  4746. })();
  4747. }
  4748. if(!dojo._hasResource["qd.app.queue"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  4749. dojo._hasResource["qd.app.queue"] = true;
  4750. dojo.provide("qd.app.queue");
  4751. (function(){
  4752. var _queueTemplate = null,
  4753. _atHomeTemplate = null,
  4754. _instantTemplate = null,
  4755. _historyTemplate = null,
  4756. _watchedTemplate = null,
  4757. pageCached = {},
  4758. lists = ["historyList", "watchedList", "instantList", "atHomeList", "queueList", "savedList"];
  4759. qd.app.queue = new (function(){
  4760. this.onLoad = function(/*Object*/queueList){
  4761. // summary:
  4762. // This fires on EVERY queueList that loads
  4763. // check queueList.type to determine which
  4764. };
  4765. this.onAllLoaded = function(){
  4766. // summary:
  4767. // Fires after all queues are loaded
  4768. if(qd.services.network.available){
  4769. qd.app.queue.polling.init();
  4770. }
  4771. dojo.connect(qd.services.network, "onChange", function(status){
  4772. if(status){
  4773. qd.app.queue.polling.initialized(false);
  4774. qd.app.queue.polling.init();
  4775. }
  4776. });
  4777. };
  4778. this.onChange = function(/*Object*/queueList,/*String*/typeOfChange){
  4779. // summary:
  4780. // This fires on EVERY queueList that changes
  4781. // check queueList.type to determine which
  4782. // typeOfChange: String
  4783. // add, remove, reorder
  4784. if(typeOfChange == "add" || typeOfChange == "remove"){
  4785. if(queueList.type == "queue"){
  4786. setNumInNav("numInQueueTotal", this.queueList.list.length);
  4787. }else if(queueList.type == "instant"){
  4788. setNumInNav("numInInstantTotal", this.instantList.list.length);
  4789. }
  4790. }
  4791. qd.service.queues.cache((queueList.type=="queue"?"disc":queueList.type), queueList.list);
  4792. };
  4793. this.count = 1
  4794. this.getItems = function(whichList){
  4795. // summary: Returns the items contained in the given list.
  4796. if(this[whichList]){
  4797. return this[whichList].result.items;
  4798. }
  4799. return [];
  4800. };
  4801. this.isProtectedPage = function(){
  4802. // summary: Returns true if the currently selected page should
  4803. // be protected by auth.
  4804. return dijit.byId("contentNode").selectedChildWidget.id =="queueContentNode";
  4805. };
  4806. this.inQueue = function(/* String */movieId, /* String? */queue){
  4807. // summary:
  4808. // Check to see that a movie is queued.
  4809. // movieId:
  4810. // The guid of the Netflix title to check.
  4811. // queue:
  4812. // Optional queue to check ("queue", "instant", "history",
  4813. // "watched", "atHome", "saved"). If nothing is provided,
  4814. // check all queues.
  4815. if(queue !== undefined){
  4816. if(queue.indexOf("List")==-1){
  4817. queue += "List";
  4818. }
  4819. }
  4820. var b = dojo.some(queue ? [queue] : lists, function(list){
  4821. if(this[list] && list!="historyList" && list!="watchedList"){
  4822. return this[list].inQueue(movieId);
  4823. }
  4824. }, this);
  4825. return b;
  4826. };
  4827. this.inQueueByTerm = function(/* String */term, /* String? */queue){
  4828. // summary:
  4829. // Check to see that a movie is queued.
  4830. // term:
  4831. // The movie's title (such as comes in from the RSS feeds).
  4832. // queue:
  4833. // Optional queue to check ("queue", "instant", "history",
  4834. // "watched", "atHome", "saved"). If nothing is provided,
  4835. // check all queues.
  4836. if(queue !== undefined){
  4837. if(queue.indexOf("List")==-1){
  4838. queue += "List";
  4839. }
  4840. }
  4841. var test = dojo.some(queue ? [queue]: lists, function(list){
  4842. if(this[list] && list!="historyList" && list!="watchedList"){
  4843. return this[list].inQueueByTerm(term);
  4844. }
  4845. }, this);
  4846. return test;
  4847. };
  4848. this.clearCache = function(){
  4849. // summary:
  4850. // When user logs out, we need to clear the page cache
  4851. // or else the pages will be 'unprotected'
  4852. pageCached = {};
  4853. dojo.forEach(lists, function(list){
  4854. if(this[list]){ this[list].destroy(); }
  4855. }, this);
  4856. };
  4857. dojo.connect(qd.app, "deauthorize", dojo.hitch(this, function(){
  4858. this.clearCache();
  4859. }));
  4860. this.gotoInitialPage = function(){
  4861. // summary: Switch to the starting page (Your Queue => DVD)
  4862. this.gotoMyQueueDvd();
  4863. };
  4864. this.switchPage = function(/* String */page){
  4865. // summary:
  4866. // Switch to the give sub-page of Your Queue.
  4867. // page:
  4868. // "dvd" (the default), "instant", "history", "notLoggedIn"
  4869. if(page == "dvd"){
  4870. this.gotoMyQueueDvd();
  4871. }
  4872. else if(page == "instant"){
  4873. this.gotoMyQueueInstant();
  4874. }
  4875. else if(page =="history"){
  4876. this.gotoMyQueueHistory();
  4877. }
  4878. else if(page == "notLoggedIn"){
  4879. qd.app.switchPage("notLoggedIn")
  4880. }
  4881. else{
  4882. this.gotoMyQueueDvd();
  4883. }
  4884. };
  4885. function changePageDisplay(/* String */page){
  4886. // summary:
  4887. // Helper function for the goto* functions, to toggle navigation and
  4888. // content elements' styles.
  4889. // page:
  4890. // "dvd", "instant", "history"
  4891. if (page == "dvd") {
  4892. qd.app.switchPage("yourQueue");
  4893. qd.app.selectNav("myQueueDvd", "queSubNav");
  4894. dijit.byId("queuePages").selectChild("queueContainerNode");
  4895. }
  4896. else if(page == "instant"){
  4897. qd.app.switchPage("yourQueue");
  4898. qd.app.selectNav("myQueueInstant", "queSubNav");
  4899. dijit.byId("queuePages").selectChild("instantContainerNode");
  4900. }
  4901. else if(page =="history"){
  4902. qd.app.switchPage("yourQueue");
  4903. qd.app.selectNav("myQueueHistory", "queSubNav");
  4904. dijit.byId("queuePages").selectChild("historyContainerNode");
  4905. }
  4906. else{
  4907. //??
  4908. }
  4909. };
  4910. this.addMovieById = function(/* String */movieId, /* Node */target, /* String */queue){
  4911. // summary:
  4912. // Adds a movie to your queue
  4913. // description:
  4914. // After cliking Add Movie in one of the areas
  4915. // of the app, the movieId is sent here. The actual
  4916. // item is retrieved (somehow) and the data is sent
  4917. // to NetFlix.
  4918. // movieId:
  4919. // Netflix title guid.
  4920. // target:
  4921. // DOM node representing the item.
  4922. // queue:
  4923. // "queue" (default), "instant"
  4924. if(qd.app.authorized) {
  4925. var queue = queue || "queue";
  4926. if(target) {
  4927. dojo.addClass(target, "inQueue")
  4928. }
  4929. if(this.inQueue(movieId, queue)) {
  4930. this.switchPage(queue);
  4931. this[(queue=="instant")?"instantList":"queueList"].highlight(movieId);
  4932. } else {
  4933. var movie = qd.services.item(movieId);
  4934. if(movie){
  4935. if(qd.services.network.available){
  4936. if(movie.screenFormats.length){
  4937. if(queue == "instant"){
  4938. if("instant" in movie.formats){
  4939. this.instantList.addMovie(movie);
  4940. }else{
  4941. console.warn("Attempted to add a movie to the instant queue, but it doesn't seem to be available for instant watching. Movie: " + movie.title + ", " + (movie.guid || "(no GUID)"));
  4942. }
  4943. }else{
  4944. this.queueList.addMovie(movie);
  4945. }
  4946. } else {
  4947. this.savedList.addMovie(movie);
  4948. }
  4949. } else {
  4950. qd.service.queues.addMovieById(movieId, target, queue);
  4951. qd.app.errorTooltip.show(
  4952. "The title has been stored.",
  4953. '"' + movie.title + '" will be added to your queue when the Netflix servers become available.',
  4954. true
  4955. );
  4956. }
  4957. } else {
  4958. // TODO: hit the API for movie details, or perhaps have
  4959. // qd.managers.movie.byId do the fetch for us
  4960. console.warn("Can't add movie: it doesn't have full information here yet.", movieId);
  4961. }
  4962. }
  4963. }
  4964. };
  4965. this.addMovieByTerm = function(/* String */term, /* Node */target, /* String */queue){
  4966. // summary:
  4967. // This is here because we do not get guids with the RSS feeds; so
  4968. // what we do is fetch the title, and the run addMovieById.
  4969. // term:
  4970. // Netflix title's title.
  4971. // target:
  4972. // DOM node representing the item.
  4973. // queue:
  4974. // "queue" (default), "instant"
  4975. if(this.inQueueByTerm(term, queue)){
  4976. var id = qd.services.itemByTerm(term).guid;
  4977. // figure out which queue it is.
  4978. if(queue === undefined){
  4979. queue = this.instantList.inQueue(id) ? "instant" : "queue";
  4980. }
  4981. this.switchPage(queue);
  4982. this[(queue=="instant")?"instantList":"queueList"].highlight(id);
  4983. } else {
  4984. if(qd.services.network.available){
  4985. qd.service.titles.fetch({
  4986. term: term,
  4987. result: dojo.hitch(this, function(item){
  4988. this.addMovieById(item.guid, target, queue);
  4989. })
  4990. });
  4991. } else {
  4992. if(target) {
  4993. dojo.addClass(target, "inQueue")
  4994. }
  4995. qd.service.queues.addMovieByTerm(term, target, queue);
  4996. qd.app.errorTooltip.show(
  4997. "The title has been stored.",
  4998. '"' + term + '" will be added to your queue when the Netflix servers become available.',
  4999. true
  5000. );
  5001. }
  5002. }
  5003. };
  5004. function setNumInNav(/* String */divId, /* Number */num){
  5005. // summary:
  5006. // Helper method to set the item count in navigation sub tabs
  5007. // divId:
  5008. // DOM node ID of the tab's label.
  5009. // num:
  5010. // Count to display.
  5011. dojo.byId(divId).innerHTML = num ? "("+num+")" : "";
  5012. }
  5013. this.getRatings = function(items, callback){
  5014. // summary:
  5015. // Get ratings (user, predicted, average) for a list of objects
  5016. // items: Array
  5017. // An array of items. Not widgets, but objects returned from NetFlix.
  5018. // callback: Function
  5019. // The function to be called upon each return. Note this will be
  5020. // called multiple times if there are more than 50 items.
  5021. var guids = dojo.map(items, function(item){
  5022. return item.guid;
  5023. });
  5024. return qd.service.titles.rated({
  5025. guids: guids,
  5026. result: callback
  5027. });
  5028. };
  5029. // atHome, discs, instant, watched, shipped, returned
  5030. this.gotoMyQueueDvd = function(){
  5031. // summary:
  5032. // Navigate to the DVD tab in Your Queue
  5033. if(!qd.app.authorized){
  5034. qd.app.switchPage("auth");
  5035. return;
  5036. }
  5037. if(pageCached["dvd"]){
  5038. changePageDisplay("dvd");
  5039. return;
  5040. }
  5041. // TODO: figure out if this would ever be loaded in the background.
  5042. qd.app.underlay.show();
  5043. var res = [];
  5044. qd.service.queues.atHome().addCallback(this, function(arr){
  5045. res = res.concat(arr.slice(0));
  5046. this.atHomeList = new qd.app.queueList({
  5047. result: { items: arr },
  5048. type: "at_home"
  5049. }, "queueAtHomeTemplateNode");
  5050. setNumInNav("numInQueueTotal", arr.length);
  5051. qd.service.queues.cache(this.atHomeList.type, this.atHomeList.list);
  5052. changePageDisplay("dvd");
  5053. qd.service.queues.discs().addCallback(this, function(arr){
  5054. res = res.concat(arr.slice(0));
  5055. this.queueList = new qd.app.queueList({
  5056. result: { items: arr },
  5057. type:"queue",
  5058. canDrag:true
  5059. }, "queueTemplateNode");
  5060. setNumInNav("numInQueueTotal", res.length);
  5061. setNumInNav("numInQueueQueued", arr.length);
  5062. qd.service.queues.cache("disc", this.queueList.list);
  5063. qd.service.queues.saved().addCallback(this, function(arr){
  5064. this.savedList = new qd.app.queueList({
  5065. result: { items: arr },
  5066. type:"saved"
  5067. }, "savedQueueTemplateNode");
  5068. setNumInNav("numInSavedQueued", arr.length);
  5069. qd.service.queues.cache(this.savedList.type, this.savedList.list);
  5070. this.onAllLoaded();
  5071. var guids = dojo.map(res, function(m){
  5072. return m.title.guid;
  5073. });
  5074. qd.service.titles.rated({
  5075. guids:guids,
  5076. result: dojo.hitch(this, function(ratingsChunk){
  5077. this.atHomeList.setRatingData(ratingsChunk);
  5078. this.queueList.setRatingData(ratingsChunk);
  5079. }),
  5080. error: dojo.hitch(this, function(err){
  5081. console.error("ratings chunk fetch error::", err);
  5082. })
  5083. }).addErrback(this, function(err){
  5084. console.error("ratings fetch error::", err);
  5085. }).addCallback(this, "gotoMyQueueInstant", true);
  5086. pageCached["dvd"] = true;
  5087. qd.app.underlay.hide();
  5088. });
  5089. }).addErrback(function(err){
  5090. qd.app.underlay.hide();
  5091. qd.app.errorTooltip.show(
  5092. "Unable to retrieve your disc queue at this time.",
  5093. "There was a communication problem getting your disc queue. Please wait a few minutes and try again."
  5094. );
  5095. });
  5096. }).addErrback(function(err){
  5097. qd.app.underlay.hide();
  5098. qd.app.errorTooltip.show(
  5099. "Unable to retrieve your At Home information at this time.",
  5100. "There was a communication problem getting your At Home information. Please wait a few minutes and try again."
  5101. );
  5102. });
  5103. };
  5104. this.gotoMyQueueInstant = function(/* Boolean */inBackground){
  5105. // summary:
  5106. // Navigate to the Instant tab in Your Queue
  5107. // inBackground:
  5108. // Pass true to skip changing the display (useful for
  5109. // loading the contents of the Instant queue but not
  5110. // actually navigating to the tab).
  5111. if(!inBackground && pageCached["instant"]){
  5112. changePageDisplay("instant");
  5113. return;
  5114. }
  5115. if(!inBackground){
  5116. qd.app.underlay.show();
  5117. } else {
  5118. qd.app.loadingIcon.show();
  5119. }
  5120. var res = [];
  5121. qd.service.queues.watched().addCallback(this, function(arr){
  5122. res = res.concat(arr.slice(0));
  5123. this.watchedList = new qd.app.queueList({
  5124. result: { items: arr },
  5125. type: "watched"
  5126. }, "instantWatchedTemplateNode");
  5127. qd.service.queues.cache(this.watchedList.type, this.watchedList.list);
  5128. qd.service.queues.instant().addCallback(this, function(arr){
  5129. res = res.concat(arr.slice(0));
  5130. this.instantList = new qd.app.queueList({
  5131. result: { items: arr },
  5132. type: "instant",
  5133. canDrag: true
  5134. }, "instantQueuedTemplateNode");
  5135. setNumInNav("numInInstantTotal", arr.length);
  5136. setNumInNav("numInInstantQueued", arr.length);
  5137. qd.service.queues.cache("instant", this.instantList.list);
  5138. var guids = dojo.map(res, function(m){
  5139. return m.title.guid;
  5140. });
  5141. qd.service.titles.rated({
  5142. guids:guids,
  5143. result: dojo.hitch(this, function(ratingsChunk){
  5144. this.watchedList.setRatingData(ratingsChunk);
  5145. this.instantList.setRatingData(ratingsChunk);
  5146. }),
  5147. error: dojo.hitch(this, function(err){
  5148. console.error("ratings chunk fetch error::", err);
  5149. })
  5150. }).addErrback(this, function(err){
  5151. console.error("ratings fetch error::", err);
  5152. })//.addCallback(this, "gotoMyQueueHistory", inBackground);
  5153. pageCached["instant"] = true;
  5154. if(!inBackground){
  5155. qd.app.underlay.hide();
  5156. } else {
  5157. qd.app.loadingIcon.hide();
  5158. }
  5159. }).addErrback(function(err){
  5160. if(!inBackground){
  5161. qd.app.underlay.hide();
  5162. } else {
  5163. qd.app.loadingIcon.hide();
  5164. }
  5165. qd.app.errorTooltip.show(
  5166. "Unable to retrieve your instant queue at this time.",
  5167. "There was a communication problem getting your instant queue. Please wait a few minutes and try again."
  5168. );
  5169. });
  5170. }).addErrback(function(err){
  5171. if(!inBackground){
  5172. qd.app.underlay.hide();
  5173. } else {
  5174. qd.app.loadingIcon.hide();
  5175. }
  5176. qd.app.errorTooltip.show(
  5177. "Unable to retrieve your instant history at this time.",
  5178. "There was a communication problem getting your instant history. Please wait a few minutes and try again."
  5179. );
  5180. });
  5181. };
  5182. this.gotoMyQueueHistory = function(inBackground){
  5183. // summary:
  5184. // Navigate to the History tab in Your Queue
  5185. // inBackground:
  5186. // Pass true to skip changing the display (useful for
  5187. // loading the contents of the History queue but not
  5188. // actually navigating to the tab).
  5189. if(!inBackground && pageCached["history"]){
  5190. changePageDisplay("history");
  5191. return;
  5192. }
  5193. if(!inBackground){
  5194. qd.app.underlay.show();
  5195. } else {
  5196. qd.app.loadingIcon.show();
  5197. }
  5198. // FIXME: we cache the merged list, not two separate ones...so we may have to alter this
  5199. // for offline.
  5200. qd.service.queues.returned().addCallback(this, function(ret){
  5201. qd.service.queues.shipped().addCallback(this, function(shp){
  5202. dojo.forEach(ret, function(m){
  5203. var found = dojo.some(shp, function(mm){
  5204. if(mm.title.guid == m.title.guid){
  5205. m.shipped = mm.shipped;
  5206. return true; //break loop
  5207. }
  5208. });
  5209. });
  5210. this.historyList = new qd.app.queueList({
  5211. result: { items: ret },
  5212. type: "history"
  5213. }, "historyTemplateNode");
  5214. setNumInNav("numInHistoryTotal", ret.length);
  5215. setNumInNav("numInHistoryQueued", ret.length);
  5216. qd.service.queues.cache(this.historyList.type, this.historyList.list);
  5217. pageCached["history"] = true;
  5218. if(!inBackground){
  5219. changePageDisplay("history");
  5220. qd.app.underlay.hide();
  5221. } else {
  5222. qd.app.loadingIcon.hide();
  5223. }
  5224. // Fetch the ratings.
  5225. setTimeout(dojo.hitch(this, function(){
  5226. qd.app.loadingIcon.show();
  5227. var guids = dojo.map(ret, function(m){
  5228. return m.title.guid;
  5229. });
  5230. qd.service.titles.rated({
  5231. guids:guids,
  5232. result: dojo.hitch(this, function(ratingsChunk){
  5233. this.historyList.setRatingData(ratingsChunk);
  5234. }),
  5235. error: dojo.hitch(this, function(err){
  5236. console.error("ratings chunk fetch error::", err);
  5237. })
  5238. }).addErrback(this, function(err){
  5239. qd.app.loadingIcon.hide();
  5240. console.error("ratings fetch error::", err);
  5241. }).addCallback(function(){
  5242. qd.app.loadingIcon.hide();
  5243. });
  5244. }), 5000);
  5245. }).addErrback(function(err){
  5246. if(!inBackground){
  5247. qd.app.underlay.hide();
  5248. } else {
  5249. qd.app.loadingIcon.hide();
  5250. }
  5251. qd.app.errorTooltip.show(
  5252. "Unable to retrieve your history at this time.",
  5253. "There was a communication problem getting your rental history. Please wait a few minutes and try again."
  5254. );
  5255. });
  5256. }).addErrback(function(err){
  5257. if(!inBackground){
  5258. qd.app.underlay.hide();
  5259. } else {
  5260. qd.app.loadingIcon.hide();
  5261. }
  5262. qd.app.errorTooltip.show(
  5263. "Unable to retrieve your history at this time.",
  5264. "There was a communication problem getting your rental history. Please wait a few minutes and try again."
  5265. );
  5266. });
  5267. }
  5268. })();
  5269. function setupNavigation(){
  5270. dojo.behavior.add({
  5271. "#bigNavYourQueue a": {
  5272. onclick:function(){
  5273. qd.app.queue.gotoMyQueueDvd();
  5274. return false;
  5275. }
  5276. },
  5277. "#myQueueDvd a": {
  5278. onclick:function(){
  5279. qd.app.queue.gotoMyQueueDvd();
  5280. return false;
  5281. }
  5282. },
  5283. "#myQueueInstant a": {
  5284. onclick:function(){
  5285. qd.app.queue.gotoMyQueueInstant();
  5286. return false;
  5287. }
  5288. },
  5289. "#myQueueHistory a": {
  5290. onclick:function(){
  5291. qd.app.queue.gotoMyQueueHistory();
  5292. return false;
  5293. }
  5294. }
  5295. });
  5296. dojo.behavior.apply();
  5297. }
  5298. dojo.addOnLoad(setupNavigation);
  5299. })();
  5300. // stuff to periodically poll the API for changes
  5301. qd.app.queue.polling = new (function(){
  5302. var pollTime = 5 * 3600,
  5303. isPolling = false,
  5304. pollInterval,
  5305. initialized = false;
  5306. this.devS = false;
  5307. this.devR = false;
  5308. this.devSR = false;
  5309. this.initialized = function(/* Boolean? */val){
  5310. // summary:
  5311. // Is the polling system initialized? With no args, acts as a getter;
  5312. // with the val arg, acts as a setter.
  5313. // val:
  5314. // Value to which to set the initialized status.
  5315. if(val !== undefined){
  5316. initialized = val;
  5317. }
  5318. return initialized;
  5319. };
  5320. this.init = function(){
  5321. // summary: Initialize the queue polling system.
  5322. if(initialized || dojo.attr(dojo.byId("receiveNotifications"), "checked") == false){ return; }
  5323. initialized = true;
  5324. var u = qd.app.user();
  5325. u.atHomeItems = null;
  5326. qd.app.user(u);
  5327. this.checkQueues(qd.app.queue.atHomeList.result.items, null);
  5328. this.checkUpdates();
  5329. };
  5330. this.dev = function(onOff){
  5331. // summary: Development helper method to force checking for updates.
  5332. setTimeout(dojo.hitch(this, "checkUpdates"), pollTime);
  5333. };
  5334. this.checkUpdates = function(){
  5335. // summary:
  5336. // Check for updates to the At Home queue.
  5337. qd.service.queues.atHome().addCallback( this, function(res){
  5338. var athome = res;
  5339. if(this.devS || this.devR || this.devSR){
  5340. this.doDevCode(athome, qd.app.queue.queueList.result.items);
  5341. }
  5342. pollInterval = setTimeout(dojo.hitch(this, "checkQueues", athome), 0);
  5343. setTimeout(dojo.hitch(this, "checkUpdates"), pollTime);
  5344. });
  5345. };
  5346. this.checkQueues = function(/*Array*/athome){
  5347. // summary:
  5348. // Compare locally stored queue items with what the Netflix API reports.
  5349. // athome:
  5350. // Array of items we think are currently At Home.
  5351. var shipped = [];
  5352. var returned = [];
  5353. var u = qd.app.user();
  5354. var found = false;
  5355. if(!u.atHomeItems || !u.atHomeItems.length){
  5356. u.atHomeItems = athome;
  5357. qd.app.save(u);
  5358. return;
  5359. }
  5360. dojo.forEach(athome, function(ah){
  5361. found = false;
  5362. dojo.forEach(u.atHomeItems, function(m){
  5363. if(m.guid == ah.guid){
  5364. if(m.shipped != ah.shipped){
  5365. shipped.push(ah);
  5366. }
  5367. if(m.returned != ah.returned){
  5368. returned.push(ah);
  5369. }
  5370. found = true;
  5371. }
  5372. });
  5373. if(!found){
  5374. // added, shipped
  5375. shipped.push(ah);
  5376. }
  5377. });
  5378. if(shipped.length && returned.length){
  5379. qd.app.systray.showShippedAndReceived(shipped, returned);
  5380. }else if(shipped.length){
  5381. qd.app.systray.showShipped(shipped);
  5382. }else if(returned.length){
  5383. qd.app.systray.showReceived(returned);
  5384. }else{
  5385. }
  5386. if (shipped.length || returned.length) {
  5387. // flush user object's
  5388. // atHomeItems and let it repopulate
  5389. // after refresh
  5390. u.atHomeItems = null;
  5391. qd.app.save();
  5392. qd.app.queue.clearCache();
  5393. qd.app.queue.atHomeList.destroy();
  5394. qd.app.queue.queueList.destroy();
  5395. //this.historyList.destroy(); //IF!
  5396. qd.app.queue.gotoMyQueueDvd();
  5397. }
  5398. };
  5399. this.doDevCode = function(/* Array */athome, /* Array */myqueue){
  5400. // summary:
  5401. // Development helper to change dates and thus force an update.
  5402. var d = new Date()
  5403. var today = dojo.date.locale.format(d, {selector:"date", datePattern:"MM/dd/yy"});
  5404. d.setDate(d.getDate()+2);
  5405. var tommorrow = dojo.date.locale.format(d, {selector:"date", datePattern:"MM/dd/yy"});
  5406. if (this.devR || this.devSR) {
  5407. var received = athome[athome.length - 1];
  5408. received.returned = today;
  5409. }
  5410. if(this.devS || this.devSR){
  5411. var updated = myqueue.shift();
  5412. updated.shipped = today;
  5413. updated.estimatedArrival = tommorrow;
  5414. athome.push(updated);
  5415. }
  5416. this.devS = this.devR = this.devSR = false;
  5417. };
  5418. })();
  5419. }
  5420. if(!dojo._hasResource["qd.app.topMovies"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  5421. dojo._hasResource["qd.app.topMovies"] = true;
  5422. dojo.provide("qd.app.topMovies");
  5423. qd.app.topMovies = new (function(){
  5424. this.switchPage = function(page){
  5425. // summary:
  5426. // Change to a Top Movies sub-page.
  5427. // page:
  5428. // "top100", "newReleases", "top25ByGenre"
  5429. this.loggedIn = qd.app.authorized;
  5430. var menuId, source, fetch=qd.app.feeds.fetch;
  5431. switch(page){
  5432. case "top100":
  5433. menuId = "topMoviesTop100";
  5434. source = {url: qd.service.feeds.top100().feed };
  5435. break;
  5436. case "newReleases":
  5437. menuId = "topMoviesNewReleases";
  5438. source = {url: qd.service.feeds.newReleases().feed };
  5439. break;
  5440. case "top25ByGenre":
  5441. menuId = "topMoviesTop25ByGenre";
  5442. source = {url: qd.app.feeds.currentTop25Feed};
  5443. break;
  5444. case "recommendations":
  5445. menuId = "topMoviesRecommendations";
  5446. qd.app.resultsList.setResultsType("recommendations");
  5447. fetch = function(){ qd.app.resultsList.fetch(arguments); }
  5448. break;
  5449. case "search":
  5450. menuId = "";
  5451. qd.app.resultsList.setResultsType("search");
  5452. fetch = null; // search does its own fetch
  5453. break;
  5454. }
  5455. qd.app.selectNav(menuId, "topMoviesSubNav");
  5456. if(fetch){ fetch(source); }
  5457. this.currentPage = page;
  5458. this.togglePageElements();
  5459. qd.app.switchPage("topMovies");
  5460. console.log("switched.");
  5461. };
  5462. this.togglePageElements = function(){
  5463. // summary:
  5464. // Show/hide certain elements of the content area according
  5465. // to the current page.
  5466. var p = this.currentPage;
  5467. dojo.style("genrePicker", "display", p=="top25ByGenre"?"inline":"none");
  5468. dojo.style("top100Title", "display", p=="top100"?"block":"none");
  5469. dojo.style("newReleasesTitle", "display", p=="newReleases"?"block":"none");
  5470. dojo.style("artworkList", "display", (p=="search"||p=="recommendations")?"none":"block");
  5471. dojo.style("searchResults", "display", (p=="search"||p=="recommendations")?"block":"none");
  5472. }
  5473. this.checkForRefresh = function(){
  5474. // summary:
  5475. // Make sure we are allowed to see the area requested.
  5476. this.togglePageElements();
  5477. if(this.loggedIn === undefined){
  5478. return;
  5479. }
  5480. if(qd.app.authorized && this.loggedIn != qd.app.authorized){
  5481. this.switchPage(this.currentPage);
  5482. this.loggedIn = qd.app.authorized;
  5483. }
  5484. dojo.style("topMoviesRecommendations", "display", qd.app.authorized ? "block" : "none");
  5485. };
  5486. dojo.behavior.add({
  5487. // Top Movies sub nav
  5488. "#topMoviesTop100 a": {
  5489. onclick:dojo.hitch(this, function(){
  5490. this.switchPage("top100");
  5491. return false;
  5492. })
  5493. },
  5494. "#topMoviesNewReleases a": {
  5495. onclick:dojo.hitch(this, function(){
  5496. this.switchPage("newReleases");
  5497. return false;
  5498. })
  5499. },
  5500. "#topMoviesTop25ByGenre a": {
  5501. onclick:dojo.hitch(this, function(){
  5502. this.switchPage("top25ByGenre");
  5503. return false;
  5504. })
  5505. },
  5506. "#topMoviesRecommendations a": {
  5507. onclick:dojo.hitch(this, function(){
  5508. this.switchPage("recommendations");
  5509. return false;
  5510. })
  5511. }
  5512. });
  5513. // lazy load the Top 100 feed when we visit Top Movies for the first time
  5514. var sectionSwitchConnect = dojo.connect(qd.app, "switchPage", dojo.hitch(this, function(page){
  5515. if(page == "topMovies" && !this.currentPage){
  5516. dojo.disconnect(sectionSwitchConnect);
  5517. this.switchPage("top100");
  5518. this.loggedIn = qd.app.authorized;
  5519. }
  5520. }));
  5521. })();
  5522. }
  5523. if(!dojo._hasResource["qd.app.feeds"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  5524. dojo._hasResource["qd.app.feeds"] = true;
  5525. dojo.provide("qd.app.feeds");
  5526. qd.app.feeds = new (function(){
  5527. this.currentTop25Feed = null;
  5528. var movieListTemplate = null,
  5529. underlay = qd.app.underlay;
  5530. this.setupTop25Picker = function(){
  5531. // summary:
  5532. // Create DOM structure and click handlers for the Genre picker
  5533. // on the Top Movies > Top 25 By Genre page.
  5534. var feeds = qd.service.feeds.list(),
  5535. gp = dojo.byId("genrePicker"),
  5536. gpl = dojo.query("ul", gp)[0],
  5537. __h = null;
  5538. dojo.connect(gp, "onclick", function(){
  5539. if(!dojo.hasClass(gp, "open")){
  5540. dojo.addClass(gp, "open");
  5541. underlay.show({loader:false, bodyOnly:false});
  5542. }else{
  5543. underlay.hide();
  5544. }
  5545. __h = dojo.connect(underlay, "hide", function(){
  5546. dojo.removeClass(gp, "open");
  5547. dojo.disconnect(__h);
  5548. });
  5549. });
  5550. dojo.forEach(feeds, function(item){
  5551. var li = document.createElement("li");
  5552. li.innerHTML = item.term;
  5553. dojo.connect(li, "onclick", dojo.hitch(this, function(evt){
  5554. this.currentTop25Feed = item.feed;
  5555. this.fetch({url:this.currentTop25Feed});
  5556. dojo.query("li", gpl).removeClass("selected");
  5557. dojo.addClass(evt.target, "selected");
  5558. dojo.byId("genrePickerSelection").innerHTML = item.term;
  5559. underlay.hide();
  5560. dojo.stopEvent(evt);
  5561. }));
  5562. // the first time through, select the first genre
  5563. if(item.feed == feeds[0].feed){ dojo.addClass(li, "selected"); }
  5564. gpl.appendChild(li);
  5565. }, this);
  5566. // select the first genre
  5567. dojo.byId("genrePickerSelection").innerHTML = feeds[0].term;
  5568. this.currentTop25Feed = feeds[0].feed;
  5569. }
  5570. this.fetch = function(/* Object */feed){
  5571. // summary:
  5572. // Fetch one of the public Netflix RSS feeds and render the
  5573. // results to the page.
  5574. // feed:
  5575. // Object containing at least one of the following members:
  5576. // * feedName ("top100"|"newReleases")
  5577. // * url (URL to a specific feed)
  5578. if(!movieListTemplate){
  5579. movieListTemplate = new dojox.dtl.HtmlTemplate("artworkList");
  5580. }
  5581. underlay.show();
  5582. qd.service.feeds.fetch({
  5583. url: feed.url,
  5584. result: function(data){
  5585. dojo.forEach(data, function(m){
  5586. m.inQueue = qd.app.queue.inQueueByTerm(m.title);
  5587. });
  5588. dojo.query("#artworkList .addButton").removeClass("inQueue");
  5589. dojo.query(".contentTop", "topMoviesContainerNode").forEach(function(node){
  5590. node.scrollTop = 0;
  5591. });
  5592. movieListTemplate.render(new dojox.dtl.Context({ catalog_titles: data }));
  5593. underlay.hide();
  5594. dojo.behavior.apply();
  5595. dojo.query("#artworkList .movie").forEach(function(node){
  5596. qd.app.movies.setupMovieId(node);
  5597. });
  5598. },
  5599. error: function(err, title){
  5600. console.warn("feeds.fetch ERROR: ", err);
  5601. underlay.hide();
  5602. qd.app.errorTooltip.show(
  5603. "The " + (title || "feed") + " is not available.",
  5604. "This feed will be available when Queued is back online."
  5605. );
  5606. }
  5607. });
  5608. }
  5609. // lazy load the genre picker
  5610. var switchConnect = dojo.connect(qd.app.topMovies, "switchPage", dojo.hitch(this, function(){
  5611. dojo.disconnect(switchConnect);
  5612. this.setupTop25Picker();
  5613. }));
  5614. })();
  5615. }
  5616. if(!dojo._hasResource["qd.app.ratings"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  5617. dojo._hasResource["qd.app.ratings"] = true;
  5618. dojo.provide("qd.app.ratings");
  5619. qd.app.ratings = new (function(){
  5620. var ratingTimeout = null;
  5621. var ratingTimeoutNode = null;
  5622. var ratingIsDisabled = false;
  5623. function setRatingType(/* Node */node, /* String */type){
  5624. // summary:
  5625. // Simple method to normalize a widget's CSS classes.
  5626. // node:
  5627. // Widget's DOM node.
  5628. // type:
  5629. // "user", "predicted", "average"
  5630. if(!dojo.hasClass(node, "starRating")){
  5631. dojo.addClass(node, "starRating");
  5632. }
  5633. dojo.removeClass(node, "user");
  5634. dojo.removeClass(node, "predicted");
  5635. dojo.removeClass(node, "average");
  5636. if(type=="user" || type=="predicted" || type=="average"){
  5637. dojo.addClass(node, type);
  5638. }
  5639. }
  5640. function buildRatingContext(/* Number|String */rating){
  5641. // summary:
  5642. // Build an object to pass to a DTL Context that describes
  5643. // a title's star rating.
  5644. // rating:
  5645. // Star rating value. 1-5, "not_interested", "no_opinion".
  5646. if(rating == "not_interested" || rating == "no_opinion"){
  5647. rating = 0;
  5648. }
  5649. for(var i=1, c={}; i<=5; i++){
  5650. // the leading spaces are here for DTL convenience
  5651. c[i] = (i <= rating) ? " full" : ((i-rating <= .5) ? " half" : " empty");
  5652. }
  5653. return c;
  5654. }
  5655. function setRatingStars(/* Node */node, /* Number|String */rating){
  5656. // summary:
  5657. // Show a particular star rating in a rating widget.
  5658. // node:
  5659. // Widget's DOM node (the one with the "starRating" CSS class).
  5660. // rating:
  5661. // Star rating value. 1-5, "not_interested", "no_opinion".
  5662. var c=1, n=node.firstChild, starClasses=buildRatingContext(rating);
  5663. while(n){
  5664. if(dojo.hasClass(n, "star")){
  5665. n.className = "star" + starClasses[c++];
  5666. }
  5667. n = n.nextSibling;
  5668. }
  5669. }
  5670. function renderRatingWidget(/* Node */node, /* String */type, /* Number|String */rating){
  5671. // summary:
  5672. // Set up CSS classes to properly display a ratings widget.
  5673. // node:
  5674. // Widget's DOM node.
  5675. // type:
  5676. // "user", "predicted", "average".
  5677. // rating:
  5678. // Star rating value. 1-5, "not_interested", "no_opinion".
  5679. var rating = rating || 0,
  5680. star = buildRatingContext(rating);
  5681. node.innerHTML = '<span class="unrate"></span>'
  5682. + '<span class="star '+star[1]+'"></span>'
  5683. + '<span class="star '+star[2]+'"></span>'
  5684. + '<span class="star '+star[3]+'"></span>'
  5685. + '<span class="star '+star[4]+'"></span>'
  5686. + '<span class="star '+star[5]+'"></span>';
  5687. setRatingType(node, type);
  5688. dojo.attr(node, "rating", rating);
  5689. }
  5690. this.buildRatingWidget = function(/* Node */node, /* String? */type, /* Number?|String? */rating, /* Boolean? */activate){
  5691. // summary:
  5692. // Put together a star rating widget to show movie star
  5693. // ratings and allow users to rate movies. The node should
  5694. // be a descendent of a node having the "movie" attribute
  5695. // containing a Netflix title ID.
  5696. // node:
  5697. // DOM node to use for the widget. For the behaviors to work
  5698. // properly, the node should have the 'starRating' CSS class;
  5699. // if it doesn't, the class will be added.
  5700. // type:
  5701. // "user", "predicted", "average"; if this isn't specified,
  5702. // it will be looked up in the cache by traversing the DOM
  5703. // to find the "movie" attribute to provide a movie ID.
  5704. // rating:
  5705. // Star rating value. 1-5, "not_interested", "no_opinion"; if
  5706. // this isn't provided, it will be looked up in the cache
  5707. // similar to the "type" parameter above.
  5708. // activate:
  5709. // Determines whether to immediately activate the widget or
  5710. // not. Defaults to false.
  5711. if(type && rating){
  5712. renderRatingWidget(node, type, rating);
  5713. if(activate){
  5714. this.activateRatingWidgets();
  5715. }
  5716. }else{
  5717. var movieId = qd.app.movies.getMovieIdByNode(node),
  5718. dfd = qd.app.movies.fetchTitle(movieId);
  5719. dfd.addCallback(this, function(movie){
  5720. var ratingType = (movie.ratings.user>0) ? "user" : ((movie.ratings.predicted>0) ? "predicted" : "average"),
  5721. rating = movie.ratings[ratingType];
  5722. renderRatingWidget(node, ratingType, rating);
  5723. if(activate){
  5724. this.activateRatingWidgets();
  5725. }
  5726. });
  5727. }
  5728. }
  5729. this.activateRatingWidgets = function(){
  5730. // summary:
  5731. // Apply event handlers to any rating widgets on the page.
  5732. dojo.behavior.apply();
  5733. };
  5734. function rebuildRatingWidgets(/* String */titleGuid){
  5735. // summary:
  5736. // Rebuilt star rating widget(s) for the title having the guid provided.
  5737. // titleGuid:
  5738. // The guid of the Netflix title in question.
  5739. var qar = qd.app.ratings;
  5740. dojo.query("[movie]").forEach(function(movieNode){
  5741. // This function can just shortcut item fetches and go straight to the
  5742. // qd.services.item() function because by definition, ratings widgets
  5743. // only exist when an item has been fetched.
  5744. var movieId = dojo.attr(movieNode, "movie");
  5745. if(movieId.indexOf("http://") != 0){ return; } // skip feed entries
  5746. if(movieId.indexOf("queues")>-1 || movieId.indexOf("rental_history")>-1 || movieId.indexOf("at_home")>-1){
  5747. movieId = qd.services.item(movieId).title.guid;
  5748. }
  5749. var item = qd.services.item(movieId);
  5750. if(item.guid == titleGuid){
  5751. dojo.query(".starRating", movieNode).forEach(function(starRatingNode){
  5752. var type = item.ratings.user ? "user" : "predicted",
  5753. rating = type=="user" ? item.ratings.user : item.ratings.predicted;
  5754. qar.buildRatingWidget(starRatingNode, type, rating);
  5755. });
  5756. }
  5757. });
  5758. qar.activateRatingWidgets();
  5759. }
  5760. function disableRatings(){
  5761. ratingIsDisabled = true;
  5762. }
  5763. function enableRatings(){
  5764. // Janky timer! Give it a brief moment to try and pass
  5765. // any click events through to nodes that might trigger
  5766. // a rating, THEN reenable it.
  5767. setTimeout(function(){
  5768. ratingIsDisabled = false;
  5769. }, 150);
  5770. }
  5771. var ratingWidgetHandler = {
  5772. widget: {
  5773. onmouseover: function(evt){
  5774. if(ratingIsDisabled){ return; }
  5775. var node = evt.target;
  5776. if(dojo.hasClass(node, "starRating") && !dojo.hasClass(node, "hovering")){
  5777. dojo.addClass(node, "hovering");
  5778. }
  5779. if(ratingTimeout && ratingTimeoutNode==node){ clearTimeout(ratingTimeout); }
  5780. },
  5781. onmouseout: function(evt){
  5782. if(ratingIsDisabled){ return; }
  5783. var node = dojo.hasClass(evt.target, "starRating") ? evt.target : evt.target.parentNode;
  5784. ratingTimeoutNode = node;
  5785. ratingTimeout = setTimeout(function(){
  5786. dojo.removeClass(node, "hovering");
  5787. setRatingStars(node, dojo.attr(node, "rating"));
  5788. }, 50);
  5789. }
  5790. },
  5791. stars: {
  5792. onmousemove: function(evt){
  5793. if(ratingIsDisabled){ return; }
  5794. var node = evt.target, n = node;
  5795. while(n = n.previousSibling){ if(dojo.hasClass(n, "star")){ n.className = "star full"; } }
  5796. n = node;
  5797. while(n = n.nextSibling){ if(dojo.hasClass(n, "star")){ n.className = "star empty"; } }
  5798. node.className = "star full";
  5799. dojo.addClass(node.parentNode, "hovering");
  5800. },
  5801. onclick: function(evt){
  5802. if(ratingIsDisabled){ return; }
  5803. var node = evt.target, n = node, p = node.parentNode, rating = 1;
  5804. var movieId = qd.app.movies.getMovieIdByNode(node);
  5805. while(n = n.previousSibling){ if(dojo.hasClass(n, "star")){ rating++; } }
  5806. dojo.removeClass(p, "average");
  5807. dojo.removeClass(p, "predicted");
  5808. dojo.addClass(p, "user");
  5809. dojo.attr(p, "rating", rating);
  5810. qd.service.titles.rate({
  5811. guid: movieId,
  5812. rating: rating
  5813. }).addCallback(function(){
  5814. rebuildRatingWidgets(movieId);
  5815. });
  5816. }
  5817. },
  5818. unrate: {
  5819. onmouseover: function(evt){
  5820. if(ratingIsDisabled){ return; }
  5821. var node = evt.target,
  5822. p = node.parentNode,
  5823. movieId = qd.app.movies.getMovieIdByNode(node),
  5824. dfd = qd.app.movies.fetchTitle(movieId);
  5825. dfd.addCallback(function(){
  5826. var fallbackRatingType = (movie.ratings && movie.ratings.predicted > 0) ? "predicted" : "average",
  5827. rating = movie.ratings[fallbackRatingType];
  5828. dojo.removeClass(p, "hovering");
  5829. setRatingStars(p, rating);
  5830. });
  5831. },
  5832. onclick: function(evt){
  5833. if(ratingIsDisabled){ return; }
  5834. var node = evt.target,
  5835. p = node.parentNode,
  5836. isUserRating = dojo.hasClass(p, "user"),
  5837. rating = isUserRating ? "no_opinion" : "not_interested",
  5838. movieId = qd.app.movies.getMovieIdByNode(node),
  5839. dfd = qd.app.movies.fetchTitle(movieId);
  5840. dfd.addCallback(function(){
  5841. var newRatingType = (movie.ratings.predicted > 0) ? "predicted" : "average",
  5842. newRating = movie.ratings[newRatingType];
  5843. qd.service.titles.rate({
  5844. guid: movieId,
  5845. rating: rating
  5846. });
  5847. dojo.removeClass(p, "user");
  5848. dojo.addClass(p, newRatingType);
  5849. dojo.attr(p, "rating", newRating);
  5850. setRatingStars(p, newRating);
  5851. console.log("Removing rating from " + movieId + "; reverting to the " + newRatingType + " value of " + newRating);
  5852. });
  5853. }
  5854. }
  5855. };
  5856. dojo.behavior.add({
  5857. ".starRating.enabled": ratingWidgetHandler.widget,
  5858. ".starRating.enabled .star": ratingWidgetHandler.stars
  5859. // TODO: disabled for now
  5860. //".starRating.enabled .unrate": ratingWidgetHandler.unrate
  5861. });
  5862. dojo.connect(qd.app, "startDragging", disableRatings);
  5863. dojo.connect(qd.app, "stopDragging", enableRatings);
  5864. })();
  5865. }
  5866. if(!dojo._hasResource["qd.app.movies"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  5867. dojo._hasResource["qd.app.movies"] = true;
  5868. dojo.provide("qd.app.movies");
  5869. qd.app.movies = new (function(){
  5870. var infoTemplate = null;
  5871. var dialogIsDisabled = false;
  5872. function getInfoDialogTemplate(){
  5873. // summary:
  5874. // Returns the DTL template for the info dialog, creating it
  5875. // if necessary.
  5876. if(!infoTemplate){
  5877. infoTemplate = new dojox.dtl.HtmlTemplate("movieInfoTemplateNode");
  5878. }
  5879. return infoTemplate;
  5880. }
  5881. function populateDialog(/* Object */movie){
  5882. // summary:
  5883. // Render the given movie data to the info dialog template.
  5884. // movie:
  5885. // Movie to render.
  5886. var template = getInfoDialogTemplate(),
  5887. context = new dojox.dtl.Context({
  5888. movie: movie,
  5889. isLoggedIn: qd.app.authorized
  5890. });
  5891. template.render(context);
  5892. // For some reason, attempting to set the button classes in DTL doesn't
  5893. // seem to be reliable, like it's caching them. So let's just post-process
  5894. // the DOM instead.
  5895. dojo.query("#movieInfoDialogNode .addButton").forEach(function(n){
  5896. var queue = dojo.hasClass(n, "instant") ? "instantList" : "queueList",
  5897. isQueued = qd.app.queue.inQueue(movie.guid, queue),
  5898. addOrRemoveClass = isQueued ? "addClass" : "removeClass";
  5899. dojo[addOrRemoveClass](n, "inQueue");
  5900. });
  5901. dojo.behavior.apply();
  5902. console.log("DIALOG MOVIE:", movie, template);
  5903. }
  5904. this.setupMovieId = function(/* Node */node, /* String? */movieId){
  5905. // summary:
  5906. // Sets the "movie" attribute on the given node to the Netflix
  5907. // movie ID encoded within. There should be a node with the CSS
  5908. // class "movie_id" somewhere inside the node's DOM; this will
  5909. // be removed and its contents used as the value of the new
  5910. // attribute. If the "movie_id" node doesn't exist, nothing
  5911. // happens.
  5912. // node:
  5913. // The node (typically one with class="movie") to mark with
  5914. // the "movie" attribute.
  5915. // movieId:
  5916. // Movie ID to use as an override, skipping the DOM traversal.
  5917. var id=0;
  5918. if(movieId){
  5919. id = movieId;
  5920. }else{
  5921. var movieIdNode = dojo.query(".movie_id", node);
  5922. if(movieIdNode && movieIdNode.length){
  5923. id = movieIdNode[0].innerHTML;
  5924. movieIdNode[0].parentNode.removeChild(movieIdNode[0]);
  5925. }else{
  5926. id = dojo.attr(node, "movie");
  5927. }
  5928. }
  5929. dojo.attr(node, "movie", id);
  5930. };
  5931. this.nodesByMovieId = function(/* String */movieId){
  5932. // summary:
  5933. // Find all nodes in the topMoviesContainerNode
  5934. //
  5935. // movieId: String
  5936. // The id of the movie
  5937. //
  5938. var nodes;
  5939. nodes = dojo.query(".movie", "topMoviesContainerNode");
  5940. if(!nodes.length){
  5941. nodes = dojo.query(".thumb", "topMoviesContainerNode");
  5942. }
  5943. return nodes; // dojo.NodeList
  5944. };
  5945. this.queueMovieChange = function(/* String */movieId, /* String */type, /* Boolean */addingTo){
  5946. // summary:
  5947. // Changes the button of a movie node to be "In Queue" or
  5948. // "not".
  5949. // type: String
  5950. // The queue in which the item is being changed: "queue", "instant", etc...
  5951. // Defaults to "queue" (the regular DVD queue).
  5952. // addingTo: Boolean
  5953. // Adding to the queue. This will add the "inQueue" class
  5954. // to the button. false or null will remove that class.
  5955. // Note that the usual case will be to remove from queue,
  5956. // as adding to queue, it is generally known which button
  5957. // to change, because that button triggered the action.
  5958. var type = type || "queue";
  5959. this.nodesByMovieId(movieId).forEach(function(n){
  5960. if(dojo.attr(n, "movie")==movieId){
  5961. dojo.query(".addButton", n).forEach(function(b){
  5962. if(type=="instant" && !dojo.hasClass(b, "instant")){ return; }
  5963. if(addingTo){
  5964. dojo.addClass(b, "inQueue");
  5965. }else{
  5966. dojo.removeClass(b, "inQueue");
  5967. }
  5968. });
  5969. }
  5970. });
  5971. };
  5972. this.getMovieIdByNode = function(/* Node */node){
  5973. // summary:
  5974. // Attempt to recover the movie ID from what's encoded
  5975. // in the DOM around a given node.
  5976. // node:
  5977. // A node which has an attribute called "movie", or a
  5978. // descendant of such a node.
  5979. // returns:
  5980. // The Netflix movie ID (which might be the title if there's
  5981. // no GUID found, which happens in the case of items in the
  5982. // RSS feeds), or 0 if not found.
  5983. while(node){
  5984. if(dojo.hasAttr(node, "movie")){
  5985. var guid = dojo.attr(node, "movie");
  5986. // test to see what this actually is.
  5987. if(guid.indexOf("api.netflix.com")==-1){
  5988. // this is a title.
  5989. return guid;
  5990. }
  5991. else if(
  5992. guid.indexOf("at_home")>-1
  5993. || guid.indexOf("queues")>-1
  5994. || guid.indexOf("rental_history")>-1
  5995. ){
  5996. // this is one of our queue things, dive into the item.
  5997. var item = qd.services.item(guid);
  5998. return (item && item.title && item.title.guid) || 0;
  5999. }
  6000. else {
  6001. return guid;
  6002. }
  6003. }
  6004. node = node.parentNode;
  6005. }
  6006. return 0;
  6007. };
  6008. this.fetchTitle = dojo.hitch(this, function(/* String */movieId){
  6009. // summary:
  6010. // Display a movie info dialog for the given movie.
  6011. // movieId:
  6012. // Netflix API item ID or title. If it starts with "http://",
  6013. // then it is assumed to be a GUID; otherwise it's taken to
  6014. // be a movie title or search term.
  6015. var arg = {};
  6016. arg[(movieId && movieId.indexOf("http://") == 0) ? "guid" : "term"] = movieId;
  6017. return qd.service.titles.fetch(arg);
  6018. });
  6019. this.showInfo = dojo.hitch(this, function(/* String */movieId){
  6020. // summary:
  6021. // Display a movie info dialog for the given movie.
  6022. // movieId:
  6023. // Netflix API item ID or title.
  6024. console.log("showing the info with movieId: " + movieId);
  6025. if(dialogIsDisabled){
  6026. console.log("Skipping the movie info dialog because it's disabled at the moment.");
  6027. return;
  6028. }
  6029. if(!movieId){
  6030. console.error("Couldn't find a movie ID!");
  6031. return;
  6032. }
  6033. if(qd.app.authorized){
  6034. // logged in; show full details
  6035. qd.app.underlay.show();
  6036. if(movieId.indexOf("queues")>-1
  6037. || movieId.indexOf("rental_history")>-1
  6038. || movieId.indexOf("at_home")>-1
  6039. ){
  6040. // get the real movie
  6041. movieId = qd.services.item(movieId).title.guid;
  6042. }
  6043. var def = this.fetchTitle(movieId);
  6044. def.addCallback(this, function(movie){
  6045. qd.app.underlay.hide();
  6046. movie.allDirectors = dojo.map(movie.directors, function(d){ return d.title; }).join(", ");
  6047. populateDialog(movie);
  6048. dojo.query(".movie", infoTemplate.getRootNode()).forEach(dojo.hitch(this, function(n){
  6049. this.setupMovieId(n, movie.guid);
  6050. }));
  6051. var ratingType = (movie.ratings.user>0) ? "user"
  6052. : ((movie.ratings.predicted>0) ? "predicted"
  6053. : "average");
  6054. dojo.query("#movieInfoTemplateNode .starRating").forEach(dojo.hitch(this, function(n){
  6055. var first = (!movie.guid.match(/titles\/discs\//) || movie.title.match(/Disc 1$/)) ? true : false;
  6056. dojo[first?"addClass":"removeClass"](n, "enabled");
  6057. dojo[first?"removeClass":"addClass"](n, "nonFirst");
  6058. qd.app.ratings.buildRatingWidget(n, ratingType, movie.ratings[ratingType], true);
  6059. }));
  6060. dijit.byId("movieInfoDialogNode").show();
  6061. });
  6062. def.addErrback(this, function(err){
  6063. qd.app.underlay.hide();
  6064. qd.app.errorTooltip.show(
  6065. "No information available.",
  6066. "There is no extended information available for this title."
  6067. );
  6068. });
  6069. }else{
  6070. // not logged in; show abbreviated info, which we should
  6071. // have because the very fact we're displaying a list of
  6072. // movies for the user to click means we've at least
  6073. // parsed one of the RSS feeds.
  6074. var movie = qd.services.item(movieId);
  6075. if(!movie){
  6076. console.error("Couldn't find movie data in the registry for title " + movieId + ".");
  6077. return;
  6078. }
  6079. populateDialog(movie);
  6080. dijit.byId("movieInfoDialogNode").show();
  6081. }
  6082. });
  6083. function disableInfoDialog(){
  6084. // summary:
  6085. // Disable showing the info dialog if the app attempts to do
  6086. // so (e.g., during a drag event).
  6087. dialogIsDisabled = true;
  6088. }
  6089. function enableInfoDialog(){
  6090. // summary:
  6091. // Enable showing the info dialog if the app attempts to do so.
  6092. // Janky timer! Give it a brief moment to try and pass
  6093. // any click events through to nodes that might trigger
  6094. // the dialog, THEN reenable it.
  6095. setTimeout(function(){
  6096. dialogIsDisabled = false;
  6097. }, 150);
  6098. }
  6099. // connect to this to listen for items being added to the queue from the info dialog
  6100. this.onTitleAddedFromDialog = function(){};
  6101. var movieInfoHandler = {
  6102. onclick: dojo.hitch(this, function(evt){
  6103. var movieId = null,
  6104. movieNode = evt.target;
  6105. while(!movieId){
  6106. movieNode = movieNode.parentNode;
  6107. movieId = dojo.attr(movieNode, "movie");
  6108. }
  6109. if(movieId){
  6110. // change the Add/In Q button state if the item gets added
  6111. // while the dialog is open
  6112. var __h = dojo.connect(this, "onTitleAddedFromDialog", function(){
  6113. dojo.disconnect(__h);
  6114. dojo.query(".addButton", movieNode).forEach(function(n){
  6115. var queue = dojo.hasClass(n, "instant") ? "instantList" : "queueList",
  6116. isQueued = qd.app.queue.inQueue(dojo.attr(movieNode, "movie"), queue);
  6117. dojo.addClass(n, "inQueue");
  6118. });
  6119. });
  6120. // show the dialog
  6121. this.showInfo(movieId);
  6122. }
  6123. })
  6124. };
  6125. var movieAddHandler = {
  6126. onclick: function(evt){
  6127. var movieId = qd.app.movies.getMovieIdByNode(evt.target);
  6128. if(movieId){
  6129. if(movieId.indexOf("http")==0){
  6130. qd.app.queue.addMovieById(movieId, evt.target);
  6131. } else {
  6132. qd.app.queue.addMovieByTerm(movieId, evt.target);
  6133. }
  6134. }
  6135. }
  6136. };
  6137. var movieDialogAddHandler = {
  6138. onclick: dojo.hitch(this, function(evt){
  6139. var movieId = qd.app.movies.getMovieIdByNode(evt.target);
  6140. if(movieId){
  6141. dijit.byId("movieInfoDialogNode").hide();
  6142. var queue = dojo.hasClass(evt.target, "instant") ? "instant" : "queue";
  6143. if(movieId.indexOf("http")==0){
  6144. qd.app.queue.addMovieById(movieId, evt.target, queue);
  6145. } else {
  6146. qd.app.queue.addMovieByTerm(movieId, evt.target, queue);
  6147. }
  6148. this.onTitleAddedFromDialog();
  6149. }
  6150. })
  6151. };
  6152. dojo.behavior.add({
  6153. // Public feed results interactions
  6154. "#artworkList .movie span.title": movieInfoHandler,
  6155. "#artworkList .movie span.boxArt": movieInfoHandler,
  6156. "#artworkList .movie span.addButton": movieAddHandler,
  6157. "#movieInfoTemplateNode .movie span.addButton": movieDialogAddHandler,
  6158. // Search results
  6159. "#searchResultsList .movie span.title": movieInfoHandler,
  6160. "#searchResultsList .movie span.boxArt": movieInfoHandler
  6161. });
  6162. dojo.connect(qd.app, "startDragging", disableInfoDialog);
  6163. dojo.connect(qd.app, "stopDragging", enableInfoDialog);
  6164. })();
  6165. }
  6166. if(!dojo._hasResource["qd.app.preferences"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  6167. dojo._hasResource["qd.app.preferences"] = true;
  6168. dojo.provide("qd.app.preferences");
  6169. (function(){
  6170. function handleExternalLink(evt){
  6171. // summary: Open a link in the system browser.
  6172. air.navigateToURL(new air.URLRequest(evt.target.href));
  6173. return false;
  6174. }
  6175. function onClickRunBackground(/*Event*/evt){
  6176. // summary: Toggle "run in background" mode.
  6177. var checkbox = evt.target;
  6178. console.log(dojo.attr(checkbox, "checked"));
  6179. var u = qd.app.user();
  6180. if(u){
  6181. u.runInBackground = dojo.attr(checkbox, "checked");
  6182. qd.app.save(u);
  6183. }
  6184. }
  6185. function onClickReceiveNotifications(/*Event*/evt){
  6186. // summary: Toggle receiving of notifications.
  6187. var checkbox = evt.target;
  6188. console.log(dojo.attr(checkbox, "checked"));
  6189. var u = qd.app.user();
  6190. if(u){
  6191. u.receiveNotifications = dojo.attr(checkbox, "checked");
  6192. qd.app.save(u);
  6193. }
  6194. }
  6195. function init(){
  6196. // summary: Startup code for the Preferences system.
  6197. // first, make links in the Preferences pane open in the system default
  6198. // browser; we can't use dojo.behavior for this because we need to
  6199. // override the actual onclick handler (so we can make it return false)
  6200. // and dojo.behavior just dojo.connect()s
  6201. dojo.query("a.extern", dojo.byId("prefsContainerNode")).forEach(function(n){
  6202. n.onclick = handleExternalLink;
  6203. });
  6204. dojo.query(".prefsAbout .prefsTitle").connect("onclick", function(evt){
  6205. if(evt.shiftKey){
  6206. air.navigateToURL(new air.URLRequest("http://www.youtube.com/watch?v=gWOzUzJd6wM"));
  6207. }
  6208. });
  6209. // next, behavior setup(s)
  6210. dojo.behavior.add({
  6211. "#deauth": {
  6212. onclick:function(){
  6213. dojo.style(dojo.byId("deauthConfirm"), "display", "block");
  6214. //qd.app.deauthorize();
  6215. return false;
  6216. }
  6217. },
  6218. "#deauthConfirmDelete": {
  6219. onclick:function(){
  6220. qd.app.deauthorize();
  6221. return false;
  6222. }
  6223. },
  6224. "#deauthConfirmKeep": {
  6225. onclick:function(){
  6226. dojo.style(dojo.byId("deauthConfirm"), "display", "none");
  6227. return false;
  6228. }
  6229. },
  6230. "#topNavPreferences a": {
  6231. onclick:function(){
  6232. qd.app.switchPage("preferences");
  6233. return false;
  6234. }
  6235. },
  6236. "#runInBackground": {
  6237. onclick:function(evt){
  6238. onClickRunBackground(evt);
  6239. return false;
  6240. }
  6241. },
  6242. "#receiveNotifications": {
  6243. onclick:function(evt){
  6244. onClickReceiveNotifications(evt);
  6245. return false;
  6246. }
  6247. }
  6248. });
  6249. dojo.behavior.apply();
  6250. // set checkboxes according to user prefs
  6251. var u = qd.app.user(),
  6252. receiveNotifications = u && u.receiveNotifications !== undefined && u.receiveNotifications || false,
  6253. runInBackground = u && u.runInBackground !== undefined && u.runInBackground || false;
  6254. dojo.attr(dojo.byId("receiveNotifications"), "checked", receiveNotifications);
  6255. dojo.attr(dojo.byId("runInBackground"), "checked", runInBackground);
  6256. }
  6257. dojo.addOnLoad(init);
  6258. })();
  6259. }
  6260. if(!dojo._hasResource["qd.app.search"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  6261. dojo._hasResource["qd.app.search"] = true;
  6262. dojo.provide("qd.app.search");
  6263. qd.app.search = new (function(){
  6264. var SUGGEST_TIMEOUT_DURATION = 250,
  6265. autoSuggestTimer = null,
  6266. suggestion = null,
  6267. term = null,
  6268. totalResultsFound = 0;
  6269. this.getClearContextData = function(){
  6270. // summary:
  6271. // Return a plain object to use with a DTL context containing data
  6272. // specific to this results list, for clearing the results template.
  6273. var qar = qd.app.resultsList;
  6274. return {
  6275. number_found: totalResultsFound,
  6276. headerClass: "search",
  6277. search_term: term,
  6278. sort_by: qar.sort
  6279. };
  6280. };
  6281. this.getSortContextData = function(){
  6282. // summary:
  6283. // Return a plain object to use with a DTL context containing data
  6284. // specific to this results list, for sorting the results template.
  6285. var qar = qd.app.resultsList;
  6286. return {
  6287. number_found: totalResultsFound,
  6288. headerClass: "search",
  6289. search_term: term
  6290. };
  6291. };
  6292. /*=====
  6293. qd.app.search.fetch.__FetchArgs = function(term, resultCount, result, error){
  6294. // summary:
  6295. // Arguments object for fetching recommendations.
  6296. // term: String
  6297. // The search term to send to Netflix.
  6298. // resultCount: Number?
  6299. // The number of results to find. Defaults to 20.
  6300. // result: Function?
  6301. // The callback function that will be executed when a result is
  6302. // fetched.
  6303. // error: Function?
  6304. // The callback function to be executed if there is an error in fetching.
  6305. }
  6306. =====*/
  6307. this.fetch = function(/* qd.app.search.fetch.__FetchArgs */kwArgs){
  6308. // summary:
  6309. // Fetch search results from the Netflix API and render them
  6310. // to the page.
  6311. var dfd = new dojo.Deferred(),
  6312. qar = qd.app.resultsList;
  6313. hideAutoSuggestPicker();
  6314. if(kwArgs && kwArgs.term){ term = kwArgs.term; }
  6315. qd.service.titles.find({
  6316. term: term,
  6317. start: qar.results.length+1,
  6318. max: (kwArgs && kwArgs.resultCount) || qar.ITEMS_PER_FETCH,
  6319. result: function(response){
  6320. totalResultsFound = response.number_found || 0;
  6321. qar.renderResults(new dojox.dtl.Context({
  6322. number_found: totalResultsFound,
  6323. search_term: term,
  6324. headerClass: "search",
  6325. results: response.results,
  6326. sort_by: "Relevance"
  6327. }));
  6328. dfd.callback(response.results);
  6329. },
  6330. error: function(err){
  6331. // TODO
  6332. }
  6333. });
  6334. return dfd;
  6335. };
  6336. this.fetchMore = function(/* qd.app.search.fetch.__FetchArgs */kwArgs){
  6337. // summary:
  6338. // Fetch additional search results from the Netflix API.
  6339. var dfd = new dojo.Deferred(),
  6340. qar = qd.app.resultsList;
  6341. qd.app.underlay.show();
  6342. qd.service.titles.find({
  6343. term: term,
  6344. start: qar.results.length+1,
  6345. max: (kwArgs && kwArgs.resultCount) || qar.ITEMS_PER_FETCH,
  6346. result: function(response){
  6347. qar.renderMoreResults(new dojox.dtl.Context({
  6348. results: response.results,
  6349. buttonClass:"addButton inQueue"
  6350. }));
  6351. dfd.callback(response.results);
  6352. },
  6353. error: function(err){
  6354. // TODO
  6355. }
  6356. });
  6357. return dfd;
  6358. };
  6359. function showAutoSuggestPicker(){
  6360. // summary:
  6361. // Reveal the auto-suggest picker.
  6362. dojo.style("searchAutoSuggest", "display", "block");
  6363. qd.app.resultsList.showPicker("searchAutoSuggestList");
  6364. }
  6365. function hideAutoSuggestPicker(){
  6366. // summary:
  6367. // Hide the auto-suggest picker.
  6368. qd.app.resultsList.hidePicker("searchAutoSuggestList", function(){
  6369. dojo.style("searchAutoSuggest", "display", "none");
  6370. });
  6371. }
  6372. function highlightSuggestion(/* String|Node */node){
  6373. // summary:
  6374. // Highlight the given auto-suggest menu item node as selected.
  6375. // node:
  6376. // The node to highlight.
  6377. if(node){
  6378. dojo.query("#searchAutoSuggest li").removeClass("selected");
  6379. dojo.addClass(node, "selected");
  6380. }
  6381. }
  6382. function autosuggest(/* String */value){
  6383. // summary:
  6384. // Grab auto-suggest data from the Netflix API and present
  6385. // it in a drop-down-like menu.
  6386. // value:
  6387. // The search term to use as the basis for the suggestions
  6388. var suggest = dojo.query("#searchAutoSuggest ul")[0];
  6389. suggest.innerHTML = "";
  6390. qd.service.titles.autosuggest({
  6391. term: value,
  6392. result: function(arr){
  6393. dojo.forEach(arr, function(item){
  6394. var li = document.createElement("li");
  6395. li.innerHTML = item;
  6396. suggest.appendChild(li);
  6397. });
  6398. if(arr.length){
  6399. showAutoSuggestPicker();
  6400. } else {
  6401. suggest.innerHTML = "<li class='nohover'><i>No suggestions</i></li>";
  6402. }
  6403. },
  6404. error: function(err){
  6405. // TODO
  6406. }
  6407. });
  6408. }
  6409. this.search = function(/* String */value){
  6410. // summary:
  6411. // Switch to the search page and run a search for the given term.
  6412. // value:
  6413. // Search term to use.
  6414. qd.app.topMovies.switchPage("search");
  6415. qd.app.resultsList.fetch({term:value});
  6416. }
  6417. dojo.behavior.add({
  6418. // Search bar
  6419. "#searchBar input": {
  6420. onkeypress:dojo.hitch(this, function(e){
  6421. var suggestNode = dojo.query("#searchAutoSuggest ul")[0];
  6422. switch(e.keyCode){
  6423. case dojo.keys.ENTER:
  6424. if(suggestion){ e.target.value = suggestion.innerHTML; }
  6425. qd.app.topMovies.switchPage("search");
  6426. qd.app.resultsList.fetch({term:e.target.value});
  6427. if(autoSuggestTimer){
  6428. clearTimeout(autoSuggestTimer);
  6429. autoSuggestTimer = null;
  6430. }
  6431. dojo.stopEvent(e);
  6432. break;
  6433. case dojo.keys.HOME:
  6434. case dojo.keys.PAGE_UP:
  6435. if(suggestion){
  6436. suggestion = suggestNode.firstChild;
  6437. dojo.stopEvent(e);
  6438. }
  6439. break;
  6440. case dojo.keys.END:
  6441. case dojo.keys.PAGE_DOWN:
  6442. if(suggestion){
  6443. suggestion = suggestNode.lastChild;
  6444. dojo.stopEvent(e);
  6445. }
  6446. break;
  6447. case dojo.keys.UP_ARROW:
  6448. if(!suggestion){
  6449. suggestion = suggestNode.lastChild;
  6450. }else{
  6451. suggestion = suggestion.previousSibling || suggestNode.lastChild;
  6452. }
  6453. break;
  6454. case dojo.keys.DOWN_ARROW:
  6455. if(!suggestion){
  6456. suggestion = suggestNode.firstChild;
  6457. }else{
  6458. suggestion = suggestion.nextSibling || suggestNode.firstChild;
  6459. }
  6460. break;
  6461. default:
  6462. // on normal keypresses, wait for a brief interval before
  6463. // checking for suggestions, to limit unnecessary API calls
  6464. if(autoSuggestTimer){
  6465. clearTimeout(autoSuggestTimer);
  6466. autoSuggestTimer = null;
  6467. }
  6468. autoSuggestTimer = setTimeout(function(){
  6469. suggestion = null;
  6470. autosuggest(e.target.value);
  6471. }, SUGGEST_TIMEOUT_DURATION);
  6472. }
  6473. highlightSuggestion(suggestion);
  6474. }),
  6475. onfocus:function(e){
  6476. if(e.target.value == "Search movies"){
  6477. e.target.value = "";
  6478. dojo.style(e.target, "color", "#000");
  6479. }
  6480. },
  6481. onblur:function(e){
  6482. // janky timeout here because we don't get the onclick event
  6483. // on #searchAutoSuggest if we hide it during this onblur; it
  6484. // goes away too before the click is registered
  6485. setTimeout(function(){
  6486. suggestion = null;
  6487. hideAutoSuggestPicker();
  6488. }, qd.app.resultsList.HIDE_TIMER_DURATION);
  6489. if(autoSuggestTimer){
  6490. clearTimeout(autoSuggestTimer);
  6491. autoSuggestTimer = null;
  6492. }
  6493. }
  6494. },
  6495. "#searchAutoSuggest ul": {
  6496. onclick:dojo.hitch(this, function(e){
  6497. term = e.target.innerHTML;
  6498. dojo.query("#searchBar input")[0].value = term;
  6499. suggestion = null;
  6500. qd.app.topMovies.switchPage("search");
  6501. qd.app.resultsList.fetch();
  6502. }),
  6503. onmouseover:function(e){
  6504. suggestion = e.target;
  6505. highlightSuggestion(suggestion);
  6506. }
  6507. }
  6508. });
  6509. })();
  6510. }
  6511. if(!dojo._hasResource["qd.app.resultsList"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  6512. dojo._hasResource["qd.app.resultsList"] = true;
  6513. dojo.provide("qd.app.resultsList");
  6514. qd.app.resultsList = new (function(){
  6515. var resultsTemplate = null,
  6516. fetchMoreTemplateNode = null,
  6517. resultsImpl = qd.app.search; // can be qd.app.recommendations or qd.app.search
  6518. this.ITEMS_PER_FETCH = 20;
  6519. this.HIDE_TIMER_DURATION = 150;
  6520. this.results = [];
  6521. this.sort = "Relevance";
  6522. this.suggestion = null;
  6523. this.autoSuggestTimer = null;
  6524. this.sortTimer = null;
  6525. this.setResultsType = function(/* String */type){
  6526. // summary:
  6527. // Set the results list to the given type.
  6528. // type:
  6529. // One of ("recommendations", "search") depending on the operation.
  6530. switch(type){
  6531. case "recommendations":
  6532. resultsImpl = qd.app.recommendations;
  6533. break;
  6534. case "search":
  6535. default:
  6536. resultsImpl = qd.app.search;
  6537. }
  6538. };
  6539. this.clearResults = function(/* Boolean */deleteFromMemory){
  6540. // summary:
  6541. // Clear the current search results.
  6542. // deleteFromMemory:
  6543. // Pass true to clear the current results from memory as well as the
  6544. // page, false to clear only the display itself. Defaults to true.
  6545. var context = {results:null};
  6546. if(typeof deleteFromMemory != "undefined" && !deleteFromMemory){
  6547. context = dojo.mixin(context, resultsImpl.getClearContextData());
  6548. }else{
  6549. this.results = [];
  6550. }
  6551. this.renderResults(new dojox.dtl.Context(context));
  6552. // normally I'd do dojo.query(...).orphan() here, but that throws an error
  6553. dojo.query(".movie", "searchResultsList").forEach(function(node){
  6554. node.parentNode.removeChild(node);
  6555. });
  6556. };
  6557. this.postProcessResults = function(results){
  6558. // summary:
  6559. // Common code to finish up post-search/recommendations/sort
  6560. // cache the results, build a guid list.
  6561. var guids = [];
  6562. if(results){
  6563. for(var i=0; i<results.length; i++){
  6564. this.results.push(results[i]);
  6565. guids.push(results[i].guid);
  6566. }
  6567. }
  6568. // put movie IDs into the DOM
  6569. var qam = qd.app.movies, inQueue;
  6570. dojo.query("#searchResults .movie").forEach(function(node){
  6571. qam.setupMovieId(node);
  6572. dojo.query(".addButton", node).forEach(function(n){
  6573. inQueue = qd.app.queue.inQueue(qam.getMovieIdByNode(n), "queueList");
  6574. dojo[inQueue ? "addClass" : "removeClass"](n, "inQueue");
  6575. });
  6576. });
  6577. // funny bug: sometimes when repeating a search or fetching recommendations
  6578. // twice in a row, our box art loses its src
  6579. var qs = qd.services;
  6580. dojo.query("#searchResults img[src=]").forEach(function(n){
  6581. dojo.attr(n, "src", qs.item(qam.getMovieIdByNode(n)).art.large);
  6582. });
  6583. qd.app.underlay.hide();
  6584. // build & activate any pending rating widgets
  6585. qd.service.titles.rated({
  6586. guids: guids,
  6587. result: function(data){
  6588. dojo.forEach(data, function(item){
  6589. var nl = dojo.query("div[movie='" + item.guid + "'] .starRating", dojo.byId("searchResultsList"));
  6590. if(nl){
  6591. // should be unique.
  6592. var rating = 3, type = "average";
  6593. if(item.ratings){
  6594. if(item.ratings.user && item.ratings.user > 0){
  6595. rating = item.ratings.user;
  6596. type = "user";
  6597. }
  6598. else if(item.ratings.predicted && item.ratings.predicted > 0){
  6599. rating = item.ratings.predicted;
  6600. type = "predicted";
  6601. }
  6602. else if(item.ratings.average && item.ratings.average > 0){
  6603. rating = item.ratings.average;
  6604. type = "average";
  6605. }
  6606. }
  6607. qd.app.ratings.buildRatingWidget(nl[0], type, rating);
  6608. }
  6609. });
  6610. }
  6611. }).addCallback(function(){
  6612. qd.app.ratings.activateRatingWidgets();
  6613. });
  6614. };
  6615. this.fetch = function(kwArgs){
  6616. // summary:
  6617. // Base method for running a search, fetching recommendations, etc.
  6618. this.clearResults();
  6619. resultsImpl.fetch(kwArgs).addCallback(this, "postProcessResults");
  6620. };
  6621. this.fetchMore = function(){
  6622. // summary:
  6623. // Pull in more results for the current batch and
  6624. // add them to the end of the existing results page.
  6625. resultsImpl.fetchMore().addCallback(this, "postProcessResults");
  6626. };
  6627. this.renderResults = function(/* Object */context){
  6628. // summary:
  6629. // Render the data in the given context to the results template.
  6630. // context:
  6631. // A dojox.dtl.Context to pass to the "results" template.
  6632. if(!resultsTemplate){
  6633. resultsTemplate = new dojox.dtl.HtmlTemplate("searchResults");
  6634. }
  6635. resultsTemplate.render(context);
  6636. };
  6637. this.renderMoreResults = function(/* Object */context){
  6638. // summary:
  6639. // Append more results onto the current results template.
  6640. // context:
  6641. // A dojox.dtl.Context to pass to the "more results" template.
  6642. var templateNode = dojo.clone(fetchMoreTemplateNode),
  6643. template = new dojox.dtl.HtmlTemplate(templateNode);
  6644. template.render(context);
  6645. dojo.byId("searchResultsList").innerHTML += template.getRootNode().innerHTML;
  6646. };
  6647. this.sortResults = function(/* String */sortField){
  6648. // summary:
  6649. // Sort the current search results.
  6650. // sortField:
  6651. // The field to sort on, one of: "Title", "Year", "Genre", "Rating", "Relevance"
  6652. // (sorting on relevance just fetches the results all over again).
  6653. this.clearResults(false);
  6654. if(sortField.toLowerCase() == "relevance"){
  6655. // just fetch them all over again, making sure to preserve the page we're on
  6656. this.fetch({resultCount:this.results.length});
  6657. return;
  6658. }
  6659. this.results.sort(function(a, b){
  6660. var ratingsMap = ["G","TV G","TV Y","TV Y7","TV Y7 FV","PG","TV PG","PG-13","TV 14","R","TV MA","UR","NR","NC-17"];
  6661. switch(sortField.toLowerCase()){
  6662. case "title":
  6663. return a.title < b.title ? -1 : 1;
  6664. case "year":
  6665. return a.releaseYear < b.releaseYear ? -1 : 1;
  6666. case "genre":
  6667. return a.categories[0] < b.categories[0] ? -1 : 1;
  6668. case "rating":
  6669. var ar = ratingsMap.indexOf(a.rating);
  6670. var br = ratingsMap.indexOf(b.rating);
  6671. if(ar == -1){ ar = 100; }
  6672. if(br == -1){ br = 100; }
  6673. return ar < br ? -1 : 1;
  6674. default:
  6675. console.log("Hmm, we're sorting by an unsupported field: " + sortField);
  6676. return -1;
  6677. }
  6678. });
  6679. var sc = resultsImpl.getSortContextData();
  6680. this.renderResults(new dojox.dtl.Context(dojo.mixin({}, sc, {
  6681. results: this.results,
  6682. sort_by: sortField
  6683. })));
  6684. this.postProcessResults();
  6685. };
  6686. this.showPicker = function(/* String|Node */node, /* Function? */onEnd){
  6687. // summary:
  6688. // Show a menu/picker by animating it in.
  6689. // node:
  6690. // DOM node to reveal.
  6691. // onEnd:
  6692. // A callback to run after the reveal completes.
  6693. var n = dojo.byId(node);
  6694. if(dojo.style(n, "display") == "none"){
  6695. dojo.style(n, {display:"block", height:"1px"});
  6696. var anim = dojo.fx.wipeIn({node:n, duration:150});
  6697. if(onEnd){
  6698. var __ac = dojo.connect(anim, "onEnd", function(){
  6699. dojo.disconnect(__ac);
  6700. onEnd();
  6701. });
  6702. }
  6703. anim.play();
  6704. }
  6705. };
  6706. this.hidePicker = function(/* String|Node */node, /* Function? */onEnd){
  6707. // summary:
  6708. // Hide a picker/menu by animating it out.
  6709. // node:
  6710. // DOM node to hide.
  6711. // onEnd:
  6712. // A callback to run after the animation completes.
  6713. var n = dojo.byId(node);
  6714. if(dojo.style(n, "display") == "block"){
  6715. var anim = dojo.fx.wipeOut({node:n, duration:75});
  6716. var __ac = dojo.connect(anim, "onEnd", function(){
  6717. dojo.disconnect(__ac);
  6718. dojo.style(n, "display", "none");
  6719. if(onEnd){ onEnd(); }
  6720. });
  6721. anim.play();
  6722. }
  6723. };
  6724. var showSortPicker = dojo.hitch(this, function(){
  6725. // summary:
  6726. // Reveal the search result sort picker.
  6727. if(!dojo.hasClass("searchResultsSortPickerSelection", "open")){
  6728. this.showPicker("searchResultsSortPicker", function(){
  6729. dojo.addClass("searchResultsSortPickerSelection", "open");
  6730. });
  6731. }
  6732. if(this.sortTimer){ clearTimeout(this.sortTimer); }
  6733. });
  6734. var hideSortPicker = dojo.hitch(this, function(){
  6735. // summary:
  6736. // Hide the search result sort picker.
  6737. this.sortTimer = setTimeout(dojo.hitch(this, function(){
  6738. this.hidePicker("searchResultsSortPicker", function(){
  6739. dojo.removeClass("searchResultsSortPickerSelection", "open");
  6740. });
  6741. }), this.HIDE_TIMER_DURATION);
  6742. });
  6743. dojo.behavior.add({
  6744. "#searchResultsSortPickerSelection": {
  6745. onmouseover: showSortPicker,
  6746. onmouseout: hideSortPicker,
  6747. },
  6748. "#searchResultsSortPicker": {
  6749. onmouseover: showSortPicker,
  6750. onmouseout: hideSortPicker,
  6751. onclick:dojo.hitch(this, function(e){
  6752. var sortField = e.target.innerHTML;
  6753. var sel = dojo.byId("searchResultsSortPickerSelection");
  6754. sel.innerHTML = sortField;
  6755. this.hidePicker("searchResultsSortPicker", function(){
  6756. dojo.removeClass(sel, "open");
  6757. });
  6758. this.sortResults(sortField);
  6759. })
  6760. },
  6761. "#searchResults .movie .addButton": {
  6762. onclick:function(evt){
  6763. var movieId = qd.app.movies.getMovieIdByNode(evt.target);
  6764. if(movieId){
  6765. qd.app.queue.addMovieById(movieId, evt.target);
  6766. }
  6767. }
  6768. },
  6769. ".searchResultsMore": {
  6770. onclick:dojo.hitch(this, function(){
  6771. this.fetchMore();
  6772. })
  6773. },
  6774. ".recommendationsMore": {
  6775. onclick:dojo.hitch(this, function(e){
  6776. this.fetchMore();
  6777. })
  6778. }
  6779. });
  6780. // lazy create the results template and "more results" template node
  6781. // when we visit Top Movies for the first time
  6782. var sectionSwitchConnect = dojo.connect(qd.app, "switchPage", dojo.hitch(this, function(page){
  6783. if(page == "topMovies" && fetchMoreTemplateNode == null){
  6784. dojo.disconnect(sectionSwitchConnect);
  6785. // set up the search and recommendations template and "more
  6786. // results" template node
  6787. var node = dojo.clone(dojo.byId("searchResultsList"));
  6788. dojo.removeAttr(node, "id");
  6789. fetchMoreTemplateNode = node;
  6790. }
  6791. }));
  6792. })();
  6793. }
  6794. if(!dojo._hasResource["qd.app.recommendations"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  6795. dojo._hasResource["qd.app.recommendations"] = true;
  6796. dojo.provide("qd.app.recommendations");
  6797. qd.app.recommendations = new (function(){
  6798. this.getClearContextData = function(){
  6799. // summary:
  6800. // Return a plain object to use with a DTL context containing data
  6801. // specific to this results list, for clearing the results template.
  6802. return {
  6803. skipCount: true,
  6804. headerClass: "recommendations",
  6805. sort_by: qd.app.resultsList.sort
  6806. };
  6807. };
  6808. this.getSortContextData = function(){
  6809. // summary:
  6810. // Return a plain object to use with a DTL context containing data
  6811. // specific to this results list, for sorting the results template.
  6812. return {
  6813. skipCount: true,
  6814. headerClass: "recommendations"
  6815. };
  6816. };
  6817. /*=====
  6818. qd.app.recommendations.fetch.__FetchArgs = function(resultCount, result, error){
  6819. // summary:
  6820. // Arguments object for fetching recommendations.
  6821. // resultCount: Number?
  6822. // The number of results to find. Defaults to 20.
  6823. // result: Function?
  6824. // The callback function that will be executed when a result is
  6825. // fetched.
  6826. // error: Function?
  6827. // The callback function to be executed if there is an error in fetching.
  6828. }
  6829. =====*/
  6830. this.fetch = function(/* qd.app.recommendations.fetch.__FetchArgs */kwArgs){
  6831. // summary:
  6832. // Fetch recommendations from the Netflix API and render
  6833. // them to the page.
  6834. var dfd = new dojo.Deferred(),
  6835. qar = qd.app.resultsList;
  6836. qd.service.titles.recommendations({
  6837. start: qar.results.length+1,
  6838. max: (kwArgs && kwArgs.resultCount) || qar.ITEMS_PER_FETCH,
  6839. result: function(results){
  6840. qar.renderResults(new dojox.dtl.Context({
  6841. skipCount: true,
  6842. headerClass: "recommendations",
  6843. results: results,
  6844. sort_by: "Relevance"
  6845. }));
  6846. dfd.callback(results);
  6847. },
  6848. error: function(err){
  6849. // TODO
  6850. }
  6851. });
  6852. return dfd;
  6853. };
  6854. this.fetchMore = function(/* qd.app.recommendations.fetch.__FetchArgs */kwArgs){
  6855. // summary:
  6856. // Fetch additional recommendations from the Netflix API.
  6857. var dfd = new dojo.Deferred(),
  6858. qar = qd.app.resultsList;
  6859. qd.app.underlay.show();
  6860. qd.service.titles.recommendations({
  6861. start: qar.results.length+1,
  6862. max: (kwArgs && kwArgs.resultCount) || qar.ITEMS_PER_FETCH,
  6863. result: function(results){
  6864. qar.renderMoreResults(new dojox.dtl.Context({
  6865. results: results,
  6866. buttonClass:"addButton inQueue"
  6867. }));
  6868. dfd.callback(results);
  6869. qd.app.underlay.hide();
  6870. },
  6871. error: function(err){
  6872. // TODO
  6873. }
  6874. });
  6875. return dfd;
  6876. };
  6877. })();
  6878. }
  6879. if(!dojo._hasResource["qd.app.systray"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  6880. dojo._hasResource["qd.app.systray"] = true;
  6881. dojo.provide("qd.app.systray");
  6882. (function(){
  6883. var di = dair.Icon;
  6884. var popWidth = 410;
  6885. var popMinHeight = 90;
  6886. var popRowHeight = 72;
  6887. var qIcon = [
  6888. 'img/icon/AIRApp_128.png',
  6889. 'img/icon/AIRApp_48.png',
  6890. 'img/icon/AIRApp_32.png',
  6891. 'img/icon/AIRApp_16.png'
  6892. ];
  6893. var getViewport = function(){
  6894. // summary: Mixin screen resolutions into viewport sniffing code
  6895. return dojo.mixin(dijit.getViewport(), {
  6896. sx: air.Capabilities.screenResolutionX,
  6897. sy: air.Capabilities.screenResolutionY
  6898. }); //Object
  6899. };
  6900. var buildWin = function(){
  6901. // summary: Build the mini At Home queue window.
  6902. var v = getViewport();
  6903. var w = popWidth;
  6904. var mr = 0;
  6905. var mb = 100;
  6906. return new dair.Window({
  6907. size:{
  6908. h:v.sy, w:w, t:0, l:v.sx - w - mr,
  6909. },
  6910. href:"Mini.html",
  6911. transparent:true,
  6912. resizable: false,
  6913. minimizable: false,
  6914. maximizable: false,
  6915. type:"utility",
  6916. systemChrome:"none",
  6917. alwaysInFront:true
  6918. }); // dair.Window
  6919. };
  6920. var getItems = function(/*String*/type){
  6921. // summary: Pull in the notifications or the At Home items.
  6922. if (type) {
  6923. return qd.app.queue.getNotifications(type);
  6924. } else {
  6925. var a = qd.app.queue.getItems.call(qd.app.queue, "atHomeList"); // Array
  6926. // make SURE we have the right image urls.
  6927. dojo.forEach(a, function(item){
  6928. item.title.art.large = qd.services.util.image.url(item.title.art.large);
  6929. item.title.art.small = qd.services.util.image.url(item.title.art.small);
  6930. });
  6931. return a;
  6932. }
  6933. };
  6934. qd.app.systray = new (function(){
  6935. // summary:
  6936. // Handles functionality that involves the taskbar icon
  6937. // and the mini window that opens when the app is in the
  6938. // background.
  6939. // NOTE:
  6940. // Consistently using the term "systray" as in the
  6941. // Lower right system tray used in Windows. However, this
  6942. // handles the Mac Dock and Dock Icon in a similar manner.
  6943. //
  6944. this.allowExit = false;
  6945. this.showing = true;
  6946. this.winshowing = false;
  6947. this.miniDisplaying = "";
  6948. this.init = function(){
  6949. // summary:
  6950. // Initialize. Set the icon in the systray,
  6951. // set menu for icon, and set up connections.
  6952. if(!di.initialized){
  6953. dojo.connect(di, "onReady", this, "init");
  6954. return;
  6955. }
  6956. di.setIcon(qIcon);
  6957. this.setMenu();
  6958. this._doConnect();
  6959. //this.win = buildWin();
  6960. };
  6961. this.showApp = function(){
  6962. // summary: Show main window
  6963. if(!this.showing){
  6964. window.nativeWindow.visible = true;
  6965. this.showing = true;
  6966. this.allowExit = false;
  6967. }
  6968. window.nativeWindow.orderToFront();
  6969. };
  6970. this.hideApp = function(){
  6971. // summary: Hide main window
  6972. if(this.showing){
  6973. window.nativeWindow.visible = false;
  6974. this.showing = false;
  6975. this.allowExit = true;
  6976. }
  6977. };
  6978. this.doSearch = function(/*String*/value){
  6979. // summary:
  6980. // Called from mini when user inserts
  6981. // a search term and hits enter
  6982. console.log("VALUE", value);
  6983. value = dojo.trim(value);
  6984. if(value){
  6985. this.showApp();
  6986. // timeout needed here or mini doesn't close
  6987. setTimeout(dojo.hitch(this, "hideMini"), 100);
  6988. qd.app.search.search(value);
  6989. }
  6990. };
  6991. this.showMini = function(){
  6992. // summary: Open the mini At Home window.
  6993. if(!this.winshowing && this.nativeWindow){
  6994. this.nativeWindow.animate("open");
  6995. this.winshowing = true;
  6996. }
  6997. };
  6998. this.hideMini = function(){
  6999. // summary:
  7000. // Hides mini.
  7001. if(this.nativeWindow && this.winshowing){
  7002. this.nativeWindow.animate("close");
  7003. this.winshowing = false;
  7004. }
  7005. };
  7006. this.isReady = function(){
  7007. // summary:
  7008. // Checks if Mini window has been built yet.
  7009. // If so, returns true. If false, builds the
  7010. // window and then retriggers original request.
  7011. if(this.nativeWindow){ return true;}
  7012. this.win = buildWin();
  7013. var callback = this.isReady.caller;
  7014. var args = this.isReady.caller.arguments;
  7015. var c = dojo.connect(this, "onWinLoad", this, function(){
  7016. dojo.disconnect(c);
  7017. callback.apply(this, args);
  7018. });
  7019. return false;
  7020. }
  7021. this.showAtHome = function(){
  7022. // summary: Show the At Home queue.
  7023. if(!this.isReady()){ return false; }
  7024. this.miniDisplaying = "atHome";
  7025. this.nativeWindow.atHome(getItems());
  7026. this.showMini();
  7027. };
  7028. this.showShipped = function(/*Array*/shipped){
  7029. // summary: Show shipped titles.
  7030. if(!this.isReady()){ return; }
  7031. this.miniDisplaying = "shipped";
  7032. this.nativeWindow.shipped(shipped || getItems("shipped"));
  7033. this.showMini();
  7034. };
  7035. this.showReceived = function(/*Array*/receieved){
  7036. // summary: Show received titles.
  7037. if(!this.isReady()){ return; }
  7038. this.miniDisplaying = "receieved";
  7039. this.nativeWindow.received(receieved ||getItems("received"));
  7040. this.showMini();
  7041. };
  7042. this.showShippedAndReceived = function(/*Array*/shipped, /*Array*/receieved){
  7043. // summary: Show both shipped and received titles.
  7044. console.log("systray.showShippedAndReceived", shipped, receieved)
  7045. if(!this.isReady()){ return; }
  7046. console.log("systray.showShippedAndReceived GO!", shipped, receieved)
  7047. this.miniDisplaying = "shippedAndReceived";
  7048. this.nativeWindow.shippedAndReceived(shipped || getItems("shipped"), receieved ||getItems("received"));
  7049. this.showMini();
  7050. };
  7051. this.devShipped = function(){
  7052. qd.app.queue.polling.dev(true);
  7053. qd.app.queue.polling.devS = true;
  7054. };
  7055. this.devReceived = function(){
  7056. qd.app.queue.polling.dev(true);
  7057. qd.app.queue.polling.devR = true;
  7058. };
  7059. this.devShippedAndReceived = function(){
  7060. qd.app.queue.polling.dev(true);
  7061. qd.app.queue.polling.devSR = true;
  7062. };
  7063. this.onListChange = function(list){
  7064. // just changes the atHome list. does not show the window.
  7065. if(this.nativeWindow && list && list.type=="at_home" && this.miniDisplaying == "atHome"){
  7066. console.info("UPDATE AT_HOME LIST", getItems().length);
  7067. this.nativeWindow.atHome(getItems());
  7068. }
  7069. };
  7070. this.onWinLoad = function(w){
  7071. console.info("MINI WINDOW LOADED", w);
  7072. this.nativeWindow = w;
  7073. };
  7074. this.onClick = function(){
  7075. // summary:
  7076. // Called when systray icon is clicked
  7077. // AND the app is not showing. If app is
  7078. // showing, this is not triggered.
  7079. // NOTE: No event, due to Mac compatibility.
  7080. this.showAtHome();
  7081. };
  7082. this.setMenu = function(){
  7083. // summary:
  7084. // Sets the right-click menu for the systray icon
  7085. // Called multiple times, and changes menu according
  7086. // to app state - like if the user is logged in.
  7087. var items = {
  7088. "Top 100 Movies": dojo.hitch(this, function(){
  7089. this.showApp();
  7090. qd.app.switchPage("topMovies");
  7091. qd.app.selectNav("", "topMoviesSubNav");
  7092. }),
  7093. "divider":true,
  7094. "Quit Queued": dojo.hitch(this, function(){
  7095. this.allowExit = true;
  7096. qd.app.exit();
  7097. })
  7098. };
  7099. if(qd.app.authorized){
  7100. items = {
  7101. "At Home Mini-Queue": dojo.hitch(this, function(){
  7102. this.showAtHome();
  7103. }),
  7104. "Your Queue": dojo.hitch(this, function(){
  7105. this.showApp();
  7106. qd.app.queue.switchPage("queue");
  7107. }),
  7108. "Top 100 Movies": dojo.hitch(this, function(){
  7109. this.showApp();
  7110. qd.app.switchPage("topMovies");
  7111. qd.app.selectNav("", "topMoviesSubNav");
  7112. }),
  7113. "Preferences": dojo.hitch(this, function(){
  7114. this.showApp();
  7115. qd.app.switchPage("preferences");
  7116. }),
  7117. "divider":true,
  7118. "Quit Queued": dojo.hitch(this, function(){
  7119. this.allowExit = true;
  7120. qd.app.exit();
  7121. })
  7122. };
  7123. }
  7124. di.setMenu(items);
  7125. };
  7126. this._doConnect = function(){
  7127. // summary:
  7128. // Building connections
  7129. // When the app is minimized, clicking the icon should
  7130. // show the Mini popup.
  7131. if(di.isTray){
  7132. // windows. supports icon click.
  7133. dojo.connect(di, "onClick", this, function(){
  7134. if(!this.showing && !this.winshowing){ this.onClick(); }
  7135. });
  7136. }else{
  7137. // Mac does not support icon click.
  7138. // the next best thing is to catch onFocus
  7139. // This will work but you'll need to blur first
  7140. // So: minimizing the app and immediately clicking
  7141. // on the button will NOT work.
  7142. dojo.connect(di, "onFocus", this, function(){
  7143. if(!this.showing && !this.winshowing){ this.onClick(); }
  7144. });
  7145. }
  7146. // some crazy handlers to allow and disallow
  7147. // the app to exit or move to the system tray
  7148. dojo.connect(window, "keypress", this, function(evt){
  7149. // if the console is open, allow keyboard exit
  7150. // else the app foobars
  7151. if(dojo.config.isDebug){
  7152. this.allowExit = true;
  7153. }
  7154. });
  7155. dojo.connect(window, "keyup", this, function(evt){
  7156. this.allowExit = false;
  7157. });
  7158. dojo.connect(window, "blur", this, function(evt){
  7159. // if the main window doesn't have focus and it is
  7160. // open don't block exit. It's most likely
  7161. // in debug mode and the console is in focus.
  7162. if(this.showing){
  7163. this.allowExit = true;
  7164. }
  7165. });
  7166. dojo.connect(window, "focus", this, function(evt){
  7167. this.allowExit = false;
  7168. });
  7169. // connecting changes to the AtHome that would show
  7170. // in the mini
  7171. dojo.connect(qd.app.queue, "onLoad", this, "onListChange");
  7172. dojo.connect(qd.app.queue, "onChange", this, "onListChange");
  7173. };
  7174. // connect the menu setting with authorization.
  7175. dojo.connect(qd.app, "authorize", dojo.hitch(this, function(){
  7176. this.setMenu();
  7177. }));
  7178. dojo.connect(qd.app, "deauthorize", dojo.hitch(this, function(){
  7179. this.setMenu();
  7180. }));
  7181. })();
  7182. var doLoad = function (){
  7183. console.log('do load ')
  7184. qd.app.systray.init();
  7185. }
  7186. function onExit(evt){
  7187. if(!qd.app.systray.allowExit && dojo.attr(dojo.byId("runInBackground"), "checked")){
  7188. evt.preventDefault();
  7189. qd.app.systray.hideApp();
  7190. }
  7191. }
  7192. window.nativeWindow.addEventListener(air.Event.CLOSING, onExit);
  7193. // dev --->
  7194. var c1, c2;
  7195. var devShow = function(){ return;
  7196. setTimeout(function(){
  7197. //qd.app.systray.showAtHome();
  7198. qd.app.systray.showShippedAndReceived();
  7199. dojo.disconnect(c1);
  7200. dojo.disconnect(c2);
  7201. }, 1000);
  7202. }
  7203. c1 = dojo.connect(qd.app, "switchPage", function(page){
  7204. if(page=="auth"){ devShow(); }
  7205. });
  7206. c2 = dojo.connect(qd.app, "hideBgLoader", function(){
  7207. devShow();
  7208. });
  7209. //
  7210. var onWin = function(evt){
  7211. //console.warn("CHANGED!", evt.type)
  7212. }
  7213. // these events all work. Keeping here for a while for reference
  7214. window.nativeWindow.addEventListener(air.NativeWindowBoundsEvent.RESIZE,onWin);
  7215. window.nativeWindow.addEventListener(air.NativeWindowBoundsEvent.RESIZING,onWin);
  7216. window.nativeWindow.addEventListener(air.NativeWindowBoundsEvent.MOVING, onWin);
  7217. window.nativeWindow.addEventListener(air.NativeWindowDisplayStateEvent.DISPLAY_STATE_CHANGING, onWin);
  7218. window.nativeWindow.addEventListener(air.NativeWindowDisplayStateEvent.DISPLAY_STATE_CHANGE, onWin);
  7219. // <-------- dev
  7220. dojo.addOnLoad(doLoad);
  7221. })();
  7222. /*
  7223. *
  7224. * for reference. would like to ani the main window.
  7225. Can't animate window
  7226. because of minWidths/heights
  7227. Need to implement a dummy window
  7228. this.restoreProps = {
  7229. x:window.nativeWindow.x,
  7230. y:window.nativeWindow.y,
  7231. w:window.nativeWindow.width,
  7232. h:window.nativeWindow.height
  7233. }
  7234. console.dir(this.getViewport())
  7235. var self = this;
  7236. dair.fx.animateWindow({
  7237. pane: window.nativeWindow,
  7238. y:500,//dair.getViewport().sy -100,
  7239. height: 100,
  7240. // easing: dojo.fx.easing.backOut,
  7241. duration:1000,
  7242. onEnd: function(){
  7243. window.nativeWindow.visible = false;
  7244. }
  7245. }).play();
  7246. */
  7247. }
  7248. if(!dojo._hasResource["qd.app.tooltip"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  7249. dojo._hasResource["qd.app.tooltip"] = true;
  7250. dojo.provide("qd.app.tooltip");
  7251. qd.app.tooltip = new (function(){
  7252. var tooltipTemplate = null,
  7253. tooltipTimer = null,
  7254. tooltipNode = null,
  7255. bestPosition = null,
  7256. STAR_WIDTH = 18, // pixel width of a single rating star
  7257. TOOLTIP_DELAY = 750; // milliseconds to wait before showing the tooltip
  7258. function getTooltipTemplate(){
  7259. // summary:
  7260. // Returns the DTL template for the tooltip, creating it
  7261. // if necessary.
  7262. if(!tooltipTemplate){
  7263. tooltipTemplate = new dojox.dtl.HtmlTemplate("movieInfoTooltipContentNode");
  7264. }
  7265. return tooltipTemplate;
  7266. }
  7267. // borrowed/ported from dijit.Tooltip
  7268. function orientTooltip(/* String */aroundCorner, /* String */tooltipCorner){
  7269. // summary:
  7270. // Set CSS to position the arrow based on which position the tooltip is in.
  7271. var node = dojo.byId("movieInfoTooltipNode"),
  7272. css = {
  7273. "TR-TL": "top right",
  7274. "BR-BL": "bottom right",
  7275. "TL-TR": "top left",
  7276. "BL-BR": "bottom left"
  7277. },
  7278. art = dojo.hasClass(node, "noArt") ? " noArt" : "";
  7279. node.className = css[aroundCorner + "-" + tooltipCorner] + art;
  7280. }
  7281. // borrowed/ported from a inner function inside dijit._place
  7282. function tryTooltipPosition(/* Object */choice){
  7283. // summary:
  7284. // Try a position for the tooltip by positioning it and checking
  7285. // the bounds against the viewport.
  7286. // choice:
  7287. // An object produced by the for loop in placeTooltip :-)
  7288. // It looks like, e.g., {aroundCorner:"TR", corner:"TL", pos:{x:0,y:0}}
  7289. var node = dojo.byId("movieInfoTooltipNode"),
  7290. corner = choice.corner,
  7291. pos = choice.pos,
  7292. view = dijit.getViewport();
  7293. orientTooltip(choice.aroundCorner, corner);
  7294. var mb = dojo.marginBox(node);
  7295. // coordinates and size of node with specified corner placed at pos,
  7296. // and clipped by viewport
  7297. var startX = (corner.charAt(1) == 'L' ? pos.x : Math.max(view.l, pos.x - mb.w)),
  7298. startY = (corner.charAt(0) == 'T' ? pos.y : Math.max(view.t, pos.y - mb.h)),
  7299. endX = (corner.charAt(1) == 'L' ? Math.min(view.l + view.w, startX + mb.w) : pos.x),
  7300. endY = (corner.charAt(0) == 'T' ? Math.min(view.t + view.h, startY + mb.h) : pos.y),
  7301. width = endX - startX,
  7302. height = endY - startY,
  7303. overflow = (mb.w - width) + (mb.h - height);
  7304. if(bestPosition == null || overflow < bestPosition.overflow){
  7305. bestPosition = {
  7306. corner: corner,
  7307. aroundCorner: choice.aroundCorner,
  7308. x: startX,
  7309. y: startY,
  7310. w: width,
  7311. h: height,
  7312. overflow: overflow
  7313. };
  7314. }
  7315. return !overflow;
  7316. }
  7317. // borrowed/ported from dijit._base.placeOnScreenAroundNode, ._placeOnScreenAroundRect,
  7318. // and ._place because something about the AIR environment breaks dojo.marginBox for
  7319. // objects with visibility="hidden", which is what dijit._place sets as part of the
  7320. // coordinate calculations
  7321. function placeTooltip(/* Node */aroundNode){
  7322. // summary:
  7323. // Position the tooltip in relation to aroundNode in such a
  7324. // way as to minimize any clipping by the viewport.
  7325. // aroundNode:
  7326. // The node for which to position the tooltip.
  7327. var align = {TR:"TL", BR:"BL", TL:"TR", BL:"BR"},
  7328. node = dojo.byId("movieInfoTooltipNode"),
  7329. pos = dojo.coords(aroundNode, true),
  7330. choices = [];
  7331. for(var nodeCorner in align){
  7332. choices.push( {
  7333. aroundCorner: nodeCorner,
  7334. corner: align[nodeCorner],
  7335. pos: {
  7336. x: pos.x + (nodeCorner.charAt(1) == 'L' ? 0 : pos.w),
  7337. y: pos.y + (nodeCorner.charAt(0) == 'T' ? 0 : pos.h)
  7338. }
  7339. });
  7340. }
  7341. bestPosition = null;
  7342. dojo.some(choices, tryTooltipPosition); // set bestPosition to the optimal choice
  7343. dojo.style(node, {left:bestPosition.x+"px", top:bestPosition.y+"px"});
  7344. orientTooltip(bestPosition.aroundCorner, bestPosition.corner);
  7345. }
  7346. function showTooltip(/* String */movieId, /* Node */aroundNode, /* Boolean? */showBoxArt){
  7347. // summary:
  7348. // Display a movie info tooltip for the given movie.
  7349. // movieId:
  7350. // Netflix API movie ID or title.
  7351. // aroundNode:
  7352. // DOM Node at which to anchor the tooltip.
  7353. // showBoxArt:
  7354. // Pass true to show box art, false otherwise. Defaults to false.
  7355. if(!movieId){
  7356. console.error("Couldn't find a movie ID!");
  7357. return;
  7358. }
  7359. if(qd.app.authorized){
  7360. qd.app.loadingIcon.show();
  7361. var def = qd.app.movies.fetchTitle(movieId);
  7362. def.addCallback(this, function(movie){
  7363. if(aroundNode == tooltipNode){ // still on the original movie node?
  7364. var node = dojo.byId("movieInfoTooltipNode"),
  7365. r = movie.ratings,
  7366. template = getTooltipTemplate(),
  7367. context = new dojox.dtl.Context({
  7368. movie: movie,
  7369. castMember1: movie.cast.length ? movie.cast[0].title : "",
  7370. castMember2: (movie.cast.length && movie.cast.length > 1) ? movie.cast[1].title : ""
  7371. });
  7372. dojo[(showBoxArt||false) ? "removeClass" : "addClass"](node, "noArt");
  7373. dojo.style(node, {display:"block", opacity:0});
  7374. template.render(context);
  7375. dojo.query(".userRatingStars", node).style("width", (r.user * STAR_WIDTH)+"px");
  7376. dojo.query(".predictedRatingStars", node).style("width", (r.predicted * STAR_WIDTH)+"px");
  7377. dojo.query(".averageRatingStars", node).style("width", (r.average * STAR_WIDTH)+"px");
  7378. placeTooltip(aroundNode);
  7379. qd.app.loadingIcon.hide();
  7380. dojo.fadeIn({node:node}).play();
  7381. }
  7382. }).addErrback(this, function(err){
  7383. qd.app.errorTooltip.show(
  7384. "No information available.",
  7385. "Could not find extended information for this title."
  7386. );
  7387. });
  7388. }
  7389. }
  7390. function hideTooltip(){
  7391. // summary:
  7392. // Hide the movie info tooltip.
  7393. dojo.style("movieInfoTooltipNode", "display", "none");
  7394. if(tooltipTimer){
  7395. clearTimeout(tooltipTimer);
  7396. tooltipTimer = null;
  7397. tooltipNode = null;
  7398. }
  7399. }
  7400. function tooltipIsDisabled(){
  7401. // summary:
  7402. // Returns true if the tooltip is disabled, else false.
  7403. return qd.app.isDragging();
  7404. }
  7405. function tooltipHandler(/* Boolean? */showBoxArt){
  7406. // summary:
  7407. // Create a handler for dojo.behavior to set up the tooltip.
  7408. // showBoxArt:
  7409. // Pass true to show box art, false otherwise. Defaults to false.
  7410. return {
  7411. onmouseover: function(evt){
  7412. if(tooltipIsDisabled()){ return; }
  7413. var node = evt.target;
  7414. var movieId = qd.app.movies.getMovieIdByNode(node);
  7415. if(movieId){
  7416. if(tooltipTimer){ hideTooltip(); }
  7417. tooltipTimer = setTimeout(function(){
  7418. tooltipNode = node;
  7419. showTooltip(movieId, node, showBoxArt);
  7420. }, TOOLTIP_DELAY);
  7421. }
  7422. },
  7423. onmouseout: function(evt){
  7424. hideTooltip();
  7425. },
  7426. onmousewheel: function(evt){
  7427. hideTooltip();
  7428. }
  7429. }
  7430. }
  7431. var tooltipCanceller = {
  7432. onclick: function(evt){
  7433. // cancel the tooltip if the user clicks somewhere
  7434. hideTooltip();
  7435. }
  7436. };
  7437. dojo.behavior.add({
  7438. ".listQueuedRow .title": dojo.mixin({}, tooltipCanceller, tooltipHandler(true)),
  7439. "#artworkList .movie .boxArt img.gloss": tooltipHandler(false),
  7440. "#artworkList .movie": tooltipCanceller
  7441. });
  7442. })();
  7443. }
  7444. if(!dojo._hasResource["qd.app.sync"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  7445. dojo._hasResource["qd.app.sync"] = true;
  7446. dojo.provide("qd.app.sync");
  7447. qd.app.sync = new (function(){
  7448. var progress=0, nActions=0, current=0;
  7449. function closeDialog(){
  7450. // summary:
  7451. // Hide the sync confirmation dialog.
  7452. dijit.byId("syncConfirmDialogNode").hide();
  7453. }
  7454. this.showDialog = function(){
  7455. // summary:
  7456. // Show the sync confirmation dialog. It always starts on the
  7457. // "question" page.
  7458. nActions = 0;
  7459. progress = 0;
  7460. current = 0;
  7461. this.switchPage("question");
  7462. dijit.byId("syncConfirmDialogNode").show();
  7463. dojo.style("progressNode", "width", "0");
  7464. };
  7465. this.switchPage = function(page){
  7466. // summary:
  7467. // Toggle the dialog page.
  7468. // page:
  7469. // One of "question", "progress".
  7470. var question, progress;
  7471. switch(page){
  7472. case "progress":
  7473. question = "none";
  7474. progress = "block";
  7475. break;
  7476. case "question":
  7477. default:
  7478. question = "block";
  7479. progress = "none";
  7480. }
  7481. dojo.style("syncQuestionNode", "display", question);
  7482. dojo.style("syncProgressNode", "display", progress);
  7483. };
  7484. this.synchronizeChanges = function(){
  7485. // summary:
  7486. // Synchronized queued changes with Netflix.
  7487. console.warn("SYNCHRONIZING CHANGES");
  7488. this.switchPage("progress");
  7489. current = 0;
  7490. qd.services.online.synchronize();
  7491. };
  7492. this.discardChanges = function(){
  7493. // summary:
  7494. // Throw away queued changes that haven't been synchronized with Netflix.
  7495. console.warn("DISCARDING CHANGES");
  7496. var h = dojo.connect(qd.services.online, "onDiscardSync", function(){
  7497. dojo.disconnect(h);
  7498. closeDialog();
  7499. });
  7500. qd.services.online.discardSynchronizations();
  7501. };
  7502. this.progress = function(/* Number? */percent){
  7503. // summary:
  7504. // Get or set the sync progress as a percentage; this is just
  7505. // for display purposes; call it to provide a visual indication
  7506. // of where the sync process is at any given time.
  7507. // percent:
  7508. // Number representing the percentage complete.
  7509. if(arguments.length){
  7510. progress = percent;
  7511. dojo.style("progressNode", "width", progress+"%");
  7512. }else{
  7513. return progress;
  7514. }
  7515. };
  7516. this.closeDialog = function(){
  7517. // summary:
  7518. // Function to expose the internal closeDialog() function as a public member of qd.app.sync.
  7519. closeDialog();
  7520. };
  7521. dojo.behavior.add({
  7522. "#syncQuestionNode .synchronizeButton": {
  7523. onclick:dojo.hitch(this, "synchronizeChanges")
  7524. },
  7525. "#syncQuestionNode .discardButton": {
  7526. onclick:dojo.hitch(this, "discardChanges")
  7527. }
  7528. });
  7529. dojo.connect(qd.services.online, "onSyncNeeded", dojo.hitch(this, function(n){
  7530. this.showDialog();
  7531. nActions = n;
  7532. }));
  7533. dojo.connect(qd.services.online, "onSyncComplete", function(){
  7534. closeDialog();
  7535. });
  7536. dojo.connect(qd.services.online, "onSyncItemStart", dojo.hitch(this, function(prompt){
  7537. dojo.byId("syncProgressPrompt").innerHTML = prompt + "...";
  7538. }));
  7539. dojo.connect(qd.services.online, "onSyncItemComplete", dojo.hitch(this, function(){
  7540. current++;
  7541. this.progress(Math.min(100, Math.round((current/nActions)*100)));
  7542. }));
  7543. })();
  7544. }
  7545. if(!dojo._hasResource["qd.init"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  7546. dojo._hasResource["qd.init"] = true;
  7547. dojo.provide("qd.init");
  7548. // PATCH ===========================>
  7549. // For some reason, the Layout widget parent (top most layout item)
  7550. // doesn't get the window bounds, it measures itself. Strange. This
  7551. // should be fixed in dijit.
  7552. dijit.layout._LayoutWidget.prototype._resize = dijit.layout._LayoutWidget.prototype.resize;
  7553. dijit.layout._LayoutWidget.prototype.resize = function(changeSize, resultSize){
  7554. // in our case this is the parent, but _LayoutWidget
  7555. // would know if it were fixed in dijit
  7556. if(this.id=="layoutNode"){
  7557. changeSize = dijit.getViewport();
  7558. }
  7559. this._resize(changeSize, resultSize);
  7560. }
  7561. //<=============================PATCH
  7562. // PATCH ===========================>
  7563. /*
  7564. dojo.Deferred.prototype.addCallback = function(cb, cbfn){
  7565. // summary:
  7566. // Add a single callback to the end of the callback sequence.
  7567. var closure = dojo.hitch.apply(dojo, arguments);
  7568. var h;
  7569. var fn = cbfn;
  7570. if(!cbfn){
  7571. h = cb;
  7572. }else{
  7573. h = function(){
  7574. var a = arguments
  7575. setTimeout(
  7576. dojo.hitch(cb, function(){
  7577. try {
  7578. closure.apply(null, a)
  7579. }catch(err){
  7580. console.error("App Error: "+ err.message)
  7581. console.dir({ "Error origination:": fn.toString() });
  7582. }
  7583. }), 0);
  7584. }
  7585. }
  7586. return this.addCallbacks(h, null); // dojo.Deferred
  7587. };
  7588. */
  7589. //<=============================PATCH
  7590. qd.init = qd.init || {};
  7591. (function(){
  7592. // setup the services getter
  7593. qd.__defineGetter__("service", function(){
  7594. // summary:
  7595. // Return the proper service (qd.services.online, qd.services.offline)
  7596. // based on the current network status.
  7597. var b = qd.services.network.available;
  7598. return b ? qd.services.online : qd.services.offline; // Object
  7599. });
  7600. dojo.addOnLoad(qd.services.init);
  7601. function setupNavigation(){
  7602. var __splash = dojo.connect(qd.app, "switchPage", function(){
  7603. dojo.disconnect(__splash);
  7604. var splash = dojo.byId("splashScreen"),
  7605. anim = dojo.fadeOut({node:splash}),
  7606. anim_h = dojo.connect(anim, "onEnd", function(){
  7607. dojo.disconnect(anim_h);
  7608. dojo.style(splash, "display", "none");
  7609. });
  7610. anim.play();
  7611. });
  7612. if(qd.app.authorized){
  7613. var h = dojo.connect(qd.services.network, "onChange", function(){
  7614. dojo.disconnect(h);
  7615. qd.app.queue.gotoInitialPage();
  7616. });
  7617. }else{
  7618. qd.app.switchPage("auth");
  7619. }
  7620. dojo.byId("searchBar").onsubmit = function(){ return false; };
  7621. dojo.query("#topNavDelivered a").connect("onclick", function(){
  7622. air.navigateToURL(new air.URLRequest("https://www.netflix.com/"));
  7623. });
  7624. // view source link
  7625. dojo.query("#viewSource").connect("onclick", function(evt){
  7626. qd.app.source();
  7627. dojo.stopEvent(evt);
  7628. return false;
  7629. });
  7630. // authorization screen
  7631. dojo.query("input.authorizeBtn").connect("onclick", function(){
  7632. qd.services.authorization.request();
  7633. });
  7634. // connect to auth and deauth.
  7635. dojo.connect(qd.app, "authorize", function(){
  7636. // get the user information
  7637. var dfd = qd.service.user.fetch();
  7638. dfd.addCallback(function(obj){
  7639. qd.app.user(obj);
  7640. dojo.byId("topNavUser").innerHTML = "Welcome " + obj.name.first + " " + obj.name.last;
  7641. dojo.byId("prefsUserName").innerHTML = obj.name.first + " " + obj.name.last;
  7642. });
  7643. qd.app.queue.gotoInitialPage();
  7644. dojo.style("searchBar", "display", "block");
  7645. dojo.removeClass(dojo.body(), "notLoggedIn");
  7646. });
  7647. dojo.connect(qd.app, "deauthorize", function(){
  7648. qd.app.user(null);
  7649. qd.app.switchPage("auth");
  7650. dojo.style("searchBar", "display", "none");
  7651. dojo.addClass(dojo.body(), "notLoggedIn");
  7652. });
  7653. // the rest
  7654. dojo.behavior.apply();
  7655. }
  7656. function setupLayout(){
  7657. // The main layout
  7658. //
  7659. // bc - all content, includes the header
  7660. var bc = new dijit.layout.BorderContainer({gutters:false}, "layoutNode");
  7661. //
  7662. // sc - all content below the header
  7663. var sc = new dijit.layout.StackContainer({region:"center", gutters:false}, "contentNode");
  7664. //s
  7665. // single pages go in sc
  7666. sc.addChild(new dijit.layout.ContentPane({region:"center"}, "prefsContainerNode"));
  7667. //
  7668. // cbc - top movies content
  7669. var cbc = new dijit.layout.BorderContainer({gutters:false}, "topMoviesContainerNode");
  7670. cbc.addChild(new dijit.layout.ContentPane({region:"top"}, "topMoviesSubNav"));
  7671. cbc.addChild(new dijit.layout.ContentPane({region:"center"}, dojo.query(".contentTop","topMoviesContainerNode")[0]));
  7672. sc.addChild(cbc);
  7673. //
  7674. // q - queued content with nav
  7675. var q = new dijit.layout.BorderContainer({gutters:false}, "queueContentNode");
  7676. q.addChild(new dijit.layout.ContentPane({region:"top"}, "queSubNav"));
  7677. sc.addChild(q);
  7678. //
  7679. // qc - queued pages
  7680. var qc = new dijit.layout.StackContainer({region:"center", gutters:false}, "queuePages");
  7681. qc.addChild(new dijit.layout.ContentPane({region:"center"}, "queueContainerNode"));
  7682. qc.addChild(new dijit.layout.ContentPane({region:"center"}, "instantContainerNode"));
  7683. qc.addChild(new dijit.layout.ContentPane({region:"center"}, "historyContainerNode"));
  7684. q.addChild(qc);
  7685. //
  7686. // a - auth content with nav
  7687. var a = new dijit.layout.BorderContainer({gutters:false}, "authContentNode");
  7688. a.addChild(new dijit.layout.ContentPane({region:"top"}, "authSubNav"));
  7689. sc.addChild(a);
  7690. //
  7691. // au - auth pages
  7692. var au = new dijit.layout.StackContainer({region:"center", gutters:false}, "authPages");
  7693. au.addChild(new dijit.layout.ContentPane({region:"center"}, "createAccountContainerNode"));
  7694. a.addChild(au);
  7695. bc.addChild(new dijit.layout.ContentPane({region:"top"}, "headerNode"));
  7696. bc.startup();
  7697. // done with main layout
  7698. // generic underlay nodes; connect to underlay.show/hide for custom behavior
  7699. // (closing a dialog, etc.); We use two underlay nodes, one for the header and
  7700. // one for the content area; this makes it easier to nest DOM nodes inside the
  7701. // content container(s)
  7702. var underlays = [ ["topMoviesUnderlay", dojo.byId("genrePicker").parentNode],
  7703. ["queueUnderlay", dojo.byId("queuePages")],
  7704. ["headerUnderlay", dojo.body()] ];
  7705. dojo.forEach(underlays, function(n){
  7706. var u = document.createElement("div");
  7707. dojo.connect(u, "onclick", qd.app.underlay, "hide");
  7708. dojo.attr(u, "id", n[0]);
  7709. dojo.place(u, n[1]);
  7710. });
  7711. // movie info dialog
  7712. (new dojox.widget.Dialog({dimensions: [800,450]}, "movieInfoDialogNode")).startup();
  7713. // sync confirmation dialog
  7714. (new dojox.widget.Dialog({dimensions: [400,185], modal:true}, "syncConfirmDialogNode")).startup();
  7715. }
  7716. dojo.addOnLoad(setupLayout);
  7717. dojo.addOnLoad(setupNavigation);
  7718. })();
  7719. }