DakuTree /
manga-tracker
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
| 1 | /* globals page, base_url, use_live_countdown_timer, list_sort_type, list_sort_order, site_aliases */ |
||
| 2 | $(function(){ |
||
| 3 | 'use strict'; |
||
| 4 | if(page !== 'dashboard_beta') { return false; } |
||
| 5 | |||
| 6 | //Set favicon to unread ver. |
||
| 7 | if(! /^\/list\//.test(location.pathname)) { |
||
| 8 | // updateFavicon(); |
||
| 9 | } |
||
| 10 | |||
| 11 | //Click to hide notice |
||
| 12 | $('#update-notice').on('closed.bs.alert', function() { |
||
| 13 | $.post(base_url + 'ajax/hide_notice'); |
||
| 14 | }); |
||
| 15 | |||
| 16 | // setupStickyListHeader(); |
||
| 17 | |||
| 18 | /** FUNCTIONS **/ |
||
| 19 | |||
| 20 | function setupStickyListHeader() { |
||
| 21 | let $window = $(window), |
||
| 22 | nav = $('#list-nav'), |
||
| 23 | offset = nav.offset().top - nav.find('ul').height()/* - 21*/, |
||
| 24 | list_table = $('table[data-list]'); |
||
| 25 | if(offset > 10) { |
||
| 26 | //normal load |
||
| 27 | $window.on('scroll', function() { |
||
| 28 | //FIXME: Using .scroll for this seems really slow. Is there no pure CSS way of doing this? |
||
| 29 | //FIXME: The width of the nav doesn't auto-adjust to change window width (since we're calcing it in JS).. |
||
| 30 | handleScroll(); |
||
| 31 | }); |
||
| 32 | handleScroll(); //Make sure we also trigger on page load. |
||
| 33 | } else { |
||
| 34 | //page was loaded via less but less hasn't parsed yet. |
||
| 35 | let existCondition = setInterval(function() { |
||
| 36 | if($('style[id="less:less-main"]').length) { |
||
| 37 | offset = nav.offset().top - nav.find('> ul').height() - 2; //reset offset |
||
| 38 | |||
| 39 | $window.on('scroll', function() { |
||
| 40 | handleScroll(); |
||
| 41 | }); |
||
| 42 | handleScroll(); //Make sure we also trigger on page load. |
||
| 43 | |||
| 44 | clearInterval(existCondition); |
||
| 45 | } |
||
| 46 | }, 500); |
||
| 47 | } |
||
| 48 | |||
| 49 | function handleScroll() { |
||
| 50 | if($window.scrollTop() >= offset) { |
||
| 51 | list_table.css('margin-top', '97px'); |
||
| 52 | nav.addClass('fixed-header'); |
||
| 53 | nav.css('width', $('#list-nav').parent().width() + 'px'); |
||
| 54 | } else { |
||
| 55 | list_table.css('margin-top', '5px'); |
||
| 56 | nav.removeClass('fixed-header'); |
||
| 57 | nav.css('width', 'initial'); |
||
| 58 | } |
||
| 59 | } |
||
| 60 | } |
||
| 61 | |||
| 62 | function updateUnread(table, row) { |
||
| 63 | let totalUnread = table.find('tr .update-read:not([style])').length, |
||
| 64 | unread_e = row.find('> td:eq(0)'), |
||
| 65 | chapter_e = row.find('> td:eq(2)'), |
||
| 66 | update_icons = row.find('.update-read, .ignore-latest'); |
||
| 67 | |||
| 68 | //Hide update icons |
||
| 69 | update_icons.hide(); |
||
| 70 | |||
| 71 | //Update updated-at time for sorting purposes. |
||
| 72 | chapter_e.attr('data-updated-at', (new Date()).toISOString().replace(/^([0-9]+-[0-9]+-[0-9]+)T([0-9]+:[0-9]+:[0-9]+)\.[0-9]+Z$/, '$1 $2')); |
||
| 73 | table.trigger('updateCell', [chapter_e[0], false, null]); |
||
| 74 | |||
| 75 | //Update unread status for sorting purposes. |
||
| 76 | unread_e.find(' > span').text('1'); |
||
| 77 | table.trigger('updateCell', [unread_e[0], false, null]); |
||
| 78 | |||
| 79 | //Update header text |
||
| 80 | let unreadText = (totalUnread > 0 ? ` (${totalUnread} unread)` : ''); |
||
| 81 | table.find('thead > tr > th:eq(1) > div').text('Series'+unreadText); |
||
| 82 | |||
| 83 | //Update data attr |
||
| 84 | table.attr('data-unread', totalUnread); |
||
| 85 | |||
| 86 | //Update favicon |
||
| 87 | if(table.attr('data-list') === 'reading') { |
||
| 88 | updateFavicon(); |
||
| 89 | } |
||
| 90 | } |
||
| 91 | |||
| 92 | function updateFavicon() { |
||
| 93 | let unreadCount = $('table[data-list=reading]').attr('data-unread').toString(); |
||
| 94 | unreadCount = parseInt(unreadCount) > 99 ? '99+' : unreadCount; |
||
| 95 | |||
| 96 | let favicon = $('link[rel="shortcut icon"]'); |
||
| 97 | if(parseInt(unreadCount) !== 0) { |
||
| 98 | let canvas = $('<canvas/>', {id: 'faviconCanvas', style: '/*display: none*/'})[0]; |
||
| 99 | //Bug?: Unable to set this via jQuery for some reason.. |
||
| 100 | canvas.width = 32; |
||
| 101 | canvas.height = 32; |
||
| 102 | |||
| 103 | let context = canvas.getContext('2d'); |
||
| 104 | |||
| 105 | let imageObj = new Image(); |
||
| 106 | imageObj.onload = function(){ |
||
| 107 | context.drawImage(imageObj, 0, 0, 32, 32); |
||
| 108 | |||
| 109 | context.font = 'Bold 17px Helvetica'; |
||
| 110 | context.textAlign = 'right'; |
||
| 111 | |||
| 112 | context.lineWidth = 3; |
||
| 113 | context.strokeStyle = 'white'; |
||
| 114 | context.strokeText(unreadCount, 32, 30); |
||
| 115 | |||
| 116 | context.fillStyle = 'black'; |
||
| 117 | context.fillText(unreadCount, 32, 30); |
||
| 118 | |||
| 119 | favicon.attr('href', canvas.toDataURL()); |
||
| 120 | }; |
||
| 121 | imageObj.src = `${base_url}favicon.ico`; |
||
| 122 | } else { |
||
| 123 | favicon.attr('href', `${base_url}favicon.ico`); |
||
| 124 | } |
||
| 125 | } |
||
| 126 | |||
| 127 | |||
| 128 | function _handleAjaxError(jqXHR, textStatus, errorThrown) { |
||
| 129 | switch(jqXHR.status) { |
||
| 130 | case 400: |
||
| 131 | alert('ERROR: ' + errorThrown); |
||
| 132 | break; |
||
| 133 | case 401: |
||
| 134 | alert('Session has expired, please re-log to continue.'); |
||
| 135 | break; |
||
| 136 | case 429: |
||
| 137 | alert('ERROR: Rate limit reached.'); |
||
| 138 | break; |
||
| 139 | default: |
||
| 140 | alert('ERROR: Something went wrong!\n'+errorThrown); |
||
| 141 | break; |
||
| 142 | } |
||
| 143 | } |
||
| 144 | |||
| 145 | function handleInactive(inactive_titles) { |
||
| 146 | //TODO: The <ul> list should be hidden by default, and only shown if a button is clicked? |
||
| 147 | |||
| 148 | let $inactiveContainer = $('#inactive-container'), |
||
| 149 | $inactiveListContainer = $inactiveContainer.find('> #inactive-list-container'), |
||
| 150 | $inactiveList = $inactiveListContainer.find('> ul'); |
||
| 151 | |||
| 152 | if(Object.keys(inactive_titles).length) { |
||
| 153 | for (let url in inactive_titles) { |
||
| 154 | if(inactive_titles.hasOwnProperty(url)) { |
||
| 155 | let domain = url.split('/')[2], |
||
| 156 | domainClass = domain; |
||
| 157 | if(site_aliases[domainClass]) { |
||
| 158 | domainClass = site_aliases[domainClass]; |
||
| 159 | } |
||
| 160 | domainClass = domainClass.replace(/\./g, '-'); |
||
| 161 | |||
| 162 | //FIXME: Don't append if already exists in list! |
||
| 163 | $('<li/>').append( |
||
| 164 | $('<i/>', {class: `sprite-site sprite-${domainClass}`, title: domain})).append( |
||
| 165 | $('<a/>', {text: ' '+inactive_titles[url], href: url}) |
||
| 166 | ).appendTo($inactiveList); |
||
| 167 | } |
||
| 168 | } |
||
| 169 | |||
| 170 | $inactiveContainer.removeAttr('hidden'); |
||
| 171 | } else { |
||
| 172 | $inactiveContainer.attr('hidden'); |
||
| 173 | |||
| 174 | $inactiveList.find('> li').empty(); |
||
| 175 | } |
||
| 176 | |||
| 177 | $('#inactive-display').on('click', function() { |
||
| 178 | $(this).hide(); |
||
| 179 | $inactiveListContainer.removeAttr('hidden'); |
||
| 180 | }); |
||
| 181 | } |
||
| 182 | |||
| 183 | let decodeEntities = (function() { |
||
| 184 | // this prevents any overhead from creating the object each time |
||
| 185 | let element = document.createElement('div'); |
||
| 186 | |||
| 187 | function decodeHTMLEntities (str) { |
||
| 188 | if(str && typeof str === 'string') { |
||
| 189 | // strip script/html tags |
||
| 190 | str = str.replace(/<script[^>]*>([\S\s]*?)<\/script>/gmi, ''); |
||
| 191 | str = str.replace(/<\/?\w(?:[^"'>]|"[^"]*"|'[^']*')*>/gmi, ''); |
||
| 192 | element.innerHTML = str; |
||
| 193 | str = element.textContent; |
||
| 194 | element.textContent = ''; |
||
| 195 | } |
||
| 196 | |||
| 197 | return str; |
||
| 198 | } |
||
| 199 | |||
| 200 | return decodeHTMLEntities; |
||
| 201 | })(); |
||
| 202 | |||
| 203 | /** |
||
| 204 | * @class TrackrApp |
||
| 205 | */ |
||
| 206 | class TrackrApp { |
||
| 207 | constructor() { |
||
| 208 | let _class = this; |
||
| 209 | |||
| 210 | this.isDashboard = !(/^\/list\//.test(location.pathname)); |
||
| 211 | |||
| 212 | this.refreshData(); |
||
| 213 | this.$tables = $('.tracker-table'); |
||
| 214 | |||
| 215 | this.enabledCategories = Object.keys(this.data.series).filter((n) => ['custom1', 'custom2', 'custom3'].includes(n)); |
||
| 216 | |||
| 217 | this.tablesorterDefaults = { |
||
| 218 | initialized: function(table) { |
||
| 219 | //fix for being unable to sort title column by asc on a single click if using "Unread (Alphabetical)" sort |
||
| 220 | //SEE: https://github.com/Mottie/tablesorter/issues/1445#issuecomment-321537911 |
||
| 221 | let sortVars = table.config.sortVars; |
||
| 222 | sortVars.forEach(function(el) { |
||
| 223 | // reset the internal counter |
||
| 224 | el.count = -1; |
||
| 225 | }); |
||
| 226 | |||
| 227 | $(table.config.headerList[4]).find('.fa-spin').remove(); |
||
| 228 | }, |
||
| 229 | |||
| 230 | //FIXME: This is kinda unneeded, and it does add a longer delay to the tablesorter load, but we need it for setting the header sort direction icons.. |
||
| 231 | sortList: _class.getListSort(list_sort_type, list_sort_order), |
||
| 232 | |||
| 233 | headers : { |
||
| 234 | 1 : { sortInitialOrder : 'asc' }, |
||
| 235 | 2 : { sortInitialOrder : 'desc', sorter: 'updated-at' }, |
||
| 236 | 3 : { sortInitialOrder : 'desc', sorter: 'latest' } |
||
| 237 | }, |
||
| 238 | |||
| 239 | textExtraction: { |
||
| 240 | 1: function (node) { |
||
| 241 | // return only the text from the text node (ignores DIV contents) |
||
| 242 | return $(node).find('.title').text(); |
||
| 243 | } |
||
| 244 | }, |
||
| 245 | |||
| 246 | widgets: ['zebra', 'filter'], |
||
| 247 | widgetOptions : { |
||
| 248 | filter_external : '#search', |
||
| 249 | filter_columnFilters: false, |
||
| 250 | filter_saveFilters : false, |
||
| 251 | filter_reset: '.reset', |
||
| 252 | filter_searchFiltered: false //FIXME: This is a temp fix for #201. More info here: https://mottie.github.io/tablesorter/docs/#widget-filter-searchfiltered |
||
| 253 | } |
||
| 254 | }; |
||
| 255 | } |
||
| 256 | |||
| 257 | refreshData() { |
||
| 258 | //FIXME: This shouldn't be async: false? |
||
| 259 | $.ajax( |
||
| 260 | { |
||
| 261 | url: base_url + 'api/internal/get_list/all', |
||
| 262 | dataType: 'json', |
||
| 263 | async: false, |
||
| 264 | success: (json) => (this.data = json) |
||
| 265 | } |
||
| 266 | ); |
||
| 267 | } |
||
| 268 | |||
| 269 | start() { |
||
| 270 | this.setupNav(); |
||
| 271 | |||
| 272 | this.generateLists(this.data.series); |
||
| 273 | this.startTablesorter(); |
||
| 274 | // handleInactive(json.extra_data.inactive_titles); |
||
| 275 | } |
||
| 276 | |||
| 277 | setupNav() { |
||
| 278 | this.setupCategoryTabs(); |
||
| 279 | |||
| 280 | this.setupUpdateTimer(); |
||
| 281 | |||
| 282 | this.setupModifySelectedEvent(); |
||
| 283 | this.setupMoveToEvent(); |
||
| 284 | |||
| 285 | //NOTE: Search event is handled via Tablesorter. |
||
| 286 | |||
| 287 | this.setupNavToggle(); |
||
| 288 | this.setupSorterEvent(); |
||
| 289 | } |
||
| 290 | setupCategoryTabs() { |
||
| 291 | let $categoryNav = $('#list-nav-category').find('> .navbar-nav'), |
||
| 292 | $moveSelect = $('#move-input'); |
||
| 293 | |||
| 294 | for (let i = 0, len = this.enabledCategories.length; i < len; i++) { |
||
| 295 | let categoryStub = this.enabledCategories[i], |
||
| 296 | categoryName = this.data.series[categoryStub].name; |
||
| 297 | |||
| 298 | $categoryNav.append( |
||
| 299 | $('<li/>').append( |
||
| 300 | $('<a/>', {href: '#', 'data-list': categoryStub, text: categoryName}) |
||
| 301 | ) |
||
| 302 | ); |
||
| 303 | |||
| 304 | $moveSelect.append( |
||
| 305 | $('<option/>', {value: categoryStub, text: categoryName}) |
||
| 306 | ); |
||
| 307 | } |
||
| 308 | |||
| 309 | //Change list when clicking category tabs |
||
| 310 | $categoryNav.find('> li > a').on('click', function(e) { |
||
| 311 | e.preventDefault(); |
||
| 312 | |||
| 313 | //Change category active state |
||
| 314 | $(this).closest('ul').find('> .active').removeClass('active'); |
||
| 315 | $(this).parent().addClass('active'); |
||
| 316 | |||
| 317 | $('.tracker-table:visible').hide(); |
||
| 318 | |||
| 319 | let datalist = $(this).attr('data-list'), |
||
| 320 | $newTable = $(`.tracker-table[data-list="${datalist}"]`); |
||
| 321 | |||
| 322 | $newTable.show(); |
||
| 323 | |||
| 324 | //Trigger update to generate even/odd rows. Tablesorter doesn't appear to auto-generate on hidden tables for some reason.. |
||
| 325 | if(!$newTable.has('.odd, .even').length) { |
||
| 326 | $newTable.trigger('update', [true]); |
||
| 327 | } |
||
| 328 | |||
| 329 | //Scroll to top of page |
||
| 330 | $('html, body').animate({ scrollTop: 0 }, 'slow'); |
||
| 331 | }); |
||
| 332 | } |
||
| 333 | setupUpdateTimer() { |
||
| 334 | if(typeof use_live_countdown_timer !== 'undefined' && use_live_countdown_timer && this.isDashboard) { |
||
| 335 | let $timer = $('#update-timer'), |
||
| 336 | timer_arr = $timer.text().split(':'), |
||
| 337 | time_left = parseInt((timer_arr[0] * 60 * 60).toString(), 10) + parseInt((timer_arr[1] * 60).toString(), 10) + parseInt(timer_arr[2], 10); |
||
| 338 | let timer = setInterval(() => { |
||
| 339 | let hours = parseInt((time_left / 60 / 60).toString(), 10), |
||
| 340 | minutes = parseInt((time_left / 60 % 60).toString(), 10), |
||
| 341 | seconds = parseInt((time_left % 60).toString(), 10); |
||
| 342 | |||
| 343 | if(hours.length === 1) { hours = '0' + hours; } |
||
| 344 | if(minutes.length === 1) { minutes = '0' + minutes; } |
||
| 345 | if(seconds.length === 1) { seconds = '0' + seconds; } |
||
| 346 | |||
| 347 | $timer.text(hours + ':' + minutes + ':' + seconds); |
||
| 348 | |||
| 349 | if (--time_left < 0) { |
||
| 350 | clearInterval(timer); |
||
| 351 | |||
| 352 | //Wait one minute, then change favicon to alert user of update |
||
| 353 | setTimeout(function(){ |
||
| 354 | //TODO: This "should" just be favicon.updated.ico, and we should handle any ENV stuff on the backend |
||
| 355 | $('link[rel*="icon"]').attr('href', `${base_url}favicon.production.updated.ico`); |
||
| 356 | |||
| 357 | //location.reload(); //TODO: We should have an option for this? |
||
| 358 | }, 60000); |
||
| 359 | } |
||
| 360 | }, 1000); |
||
| 361 | } |
||
| 362 | } |
||
| 363 | setupModifySelectedEvent() { |
||
| 364 | let _class = this; |
||
| 365 | |||
| 366 | $('#mass-action').find('> select').on('change', function() { |
||
| 367 | let redirect = false; |
||
| 368 | |||
| 369 | let $checked_rows = $('.tracker-table:visible').find('tr:has(td input[type=checkbox]:checked)'), |
||
| 370 | total_rows = $checked_rows.length; |
||
| 371 | if(total_rows > 0) { |
||
| 372 | let row_ids = $($checked_rows).map(function() { |
||
| 373 | return parseInt($(this).attr('data-id').toString()); |
||
| 374 | }).toArray(); |
||
| 375 | |||
| 376 | let postData = { |
||
| 377 | 'id[]' : row_ids |
||
| 378 | }; |
||
| 379 | switch($(this).val()) { |
||
| 380 | case 'delete': |
||
| 381 | if(confirm(`Are you sure you want to delete the ${total_rows} selected row(s)?`)) { |
||
| 382 | window.onbeforeunload = null; |
||
| 383 | $.post(base_url + 'ajax/delete_inline', postData, () => { |
||
| 384 | redirect = true; |
||
| 385 | location.reload(); |
||
| 386 | }).fail((jqXHR, textStatus, errorThrown) => { |
||
| 387 | _handleAjaxError(jqXHR, textStatus, errorThrown); |
||
| 388 | }); |
||
| 389 | } |
||
| 390 | |||
| 391 | break; |
||
| 392 | |||
| 393 | case 'tag': |
||
| 394 | if(confirm(`Are you sure you want to edit the tags of ${total_rows} selected row(s)?`)) { |
||
| 395 | let tags = prompt('Tags: '); |
||
| 396 | _class.validateTagList(tags, (tag_list_new) => { |
||
| 397 | postData.tag_string = tag_list_new; |
||
| 398 | |||
| 399 | window.onbeforeunload = null; |
||
| 400 | $.post(base_url + 'ajax/mass_tag_update', postData, () => { |
||
| 401 | redirect = true; |
||
| 402 | location.reload(); //unlike a normal tag update, it's probably better to just force a reload here. |
||
| 403 | }).fail((jqXHR, textStatus, errorThrown) => { |
||
| 404 | _handleAjaxError(jqXHR, textStatus, errorThrown); |
||
| 405 | }); |
||
| 406 | }); |
||
| 407 | } |
||
| 408 | break; |
||
| 409 | |||
| 410 | default: |
||
| 411 | //do nothing |
||
| 412 | break; |
||
| 413 | } |
||
| 414 | } else { |
||
| 415 | alert('No selected series found.'); |
||
| 416 | } |
||
| 417 | |||
| 418 | if($(this).val() !== 'n/a' && !redirect) { console.log('resetting value'); $(this).val('n/a'); } //Reset change if user hasn't followed through with mass action |
||
| 419 | }); |
||
| 420 | } |
||
| 421 | setupMoveToEvent() { |
||
| 422 | $('#move-input').on('change', function() { |
||
| 423 | let selected = $(this).find(':selected'), |
||
| 424 | selected_name = selected.text(); |
||
| 425 | if(selected.is('[value]')) { |
||
| 426 | let $checked_rows = $('.tracker-table:visible').find('tr:has(td input[type=checkbox]:checked)'), |
||
| 427 | total_rows = $checked_rows.length; |
||
| 428 | if($checked_rows.length > 0 && confirm(`Are you sure you want to move the ${total_rows} selected row(s) to the ${selected_name} category?`)) { |
||
| 429 | let row_ids = $($checked_rows).map(function() { |
||
|
0 ignored issues
–
show
|
|||
| 430 | return parseInt($(this).attr('data-id').toString()); |
||
| 431 | }).toArray(); |
||
| 432 | |||
| 433 | window.onbeforeunload = null; |
||
| 434 | $.post(base_url + 'ajax/set_category', {'id[]' : row_ids, category : selected.attr('value')}, () => { |
||
| 435 | location.reload(); |
||
| 436 | }).fail((jqXHR, textStatus, errorThrown) => { |
||
| 437 | _handleAjaxError(jqXHR, textStatus, errorThrown); |
||
| 438 | }); |
||
| 439 | } else { |
||
| 440 | $('#move-input').val('---'); |
||
| 441 | } |
||
| 442 | } |
||
| 443 | }); |
||
| 444 | } |
||
| 445 | setupNavToggle() { |
||
| 446 | $('#toggle-nav-options').on('click', function(e) { |
||
| 447 | e.preventDefault(); |
||
| 448 | |||
| 449 | let $icon = $(this).find('> i'), |
||
| 450 | $options = $('#nav-options'); |
||
| 451 | $icon.toggleClass('down'); |
||
| 452 | |||
| 453 | if($icon.hasClass('down')) { |
||
| 454 | $options.hide().slideDown(500); |
||
| 455 | } else { |
||
| 456 | $options.show().slideUp(500); |
||
| 457 | } |
||
| 458 | }); |
||
| 459 | } |
||
| 460 | setupSorterEvent() { |
||
| 461 | let _class = this; |
||
| 462 | |||
| 463 | //Setup sorter event |
||
| 464 | $('.list_sort').on('change', function() { |
||
| 465 | let $tables = $('.tracker-table'), |
||
| 466 | type = $('select[name=list_sort_type]').val(), |
||
| 467 | $order_ele = $('select[name=list_sort_order]'), |
||
| 468 | order = $order_ele.val(); |
||
| 469 | |||
| 470 | if(type === 'n/a') { return; } //do nothing, if n/a |
||
| 471 | |||
| 472 | if($(this).attr('name') === 'list_sort_type') { |
||
| 473 | //Type has changed, so set order to default. |
||
| 474 | switch(type) { |
||
| 475 | case 'unread_latest': |
||
| 476 | order = 'desc'; |
||
| 477 | break; |
||
| 478 | |||
| 479 | case 'my_status': |
||
| 480 | order = 'desc'; |
||
| 481 | break; |
||
| 482 | |||
| 483 | case 'latest': |
||
| 484 | order = 'desc'; |
||
| 485 | break; |
||
| 486 | |||
| 487 | default: |
||
| 488 | order = 'asc'; |
||
| 489 | break; |
||
| 490 | } |
||
| 491 | $order_ele.val(order); //thankfully .val doesn't re-trigger .change |
||
| 492 | } |
||
| 493 | |||
| 494 | $tables.trigger('sorton', [ _class.getListSort(type, order) ]); |
||
| 495 | }); |
||
| 496 | |||
| 497 | // Make sure order inputs are updated if sort is changed elsewhere |
||
| 498 | $('.tracker-table').on('sortEnd', function(/**e, table**/) { |
||
| 499 | let $type_ele = $('select[name=list_sort_type]'), |
||
| 500 | $order_ele = $('select[name=list_sort_order]'), |
||
| 501 | sortList = this.config.sortList, |
||
| 502 | sort = sortList.reduce(function(acc, cur/*, i*/) { |
||
| 503 | acc[cur[0]] = cur[1]; |
||
| 504 | return acc; |
||
| 505 | }, {}); |
||
| 506 | |||
| 507 | let sortType = 'n/a', |
||
| 508 | sortOrder = 'asc'; |
||
| 509 | switch(Object.keys(sort).join()) { |
||
| 510 | case '0,1': |
||
| 511 | if(sort[0] === 0) { |
||
| 512 | sortType = 'unread'; |
||
| 513 | sortOrder = (sort[1] === 0 ? 'asc' : 'desc'); |
||
| 514 | } |
||
| 515 | break; |
||
| 516 | |||
| 517 | case '0,3': |
||
| 518 | if(sort[0] === 0) { |
||
| 519 | sortType = 'unread-latest'; |
||
| 520 | sortOrder = (sort[1] === 0 ? 'asc' : 'desc'); |
||
| 521 | } |
||
| 522 | break; |
||
| 523 | |||
| 524 | case '1': |
||
| 525 | sortType = 'alphabetical'; |
||
| 526 | sortOrder = (sort[1] === 0 ? 'asc' : 'desc'); |
||
| 527 | break; |
||
| 528 | |||
| 529 | case '2': |
||
| 530 | sortType = 'my_status'; |
||
| 531 | sortOrder = (sort[2] === 0 ? 'asc' : 'desc'); |
||
| 532 | break; |
||
| 533 | |||
| 534 | case '3': |
||
| 535 | sortType = 'latest'; |
||
| 536 | sortOrder = (sort[3] === 0 ? 'asc' : 'desc'); |
||
| 537 | break; |
||
| 538 | |||
| 539 | default: |
||
| 540 | //we already default to n/a |
||
| 541 | break; |
||
| 542 | } |
||
| 543 | |||
| 544 | $type_ele.val(sortType); |
||
| 545 | $order_ele.val(sortOrder); |
||
| 546 | }); |
||
| 547 | } |
||
| 548 | |||
| 549 | generateLists(series) { |
||
| 550 | // Generate Tables |
||
| 551 | // FIXME: We should generate lists for activated categories, even if empty. |
||
| 552 | for (let [seriesStub, seriesData] of Object.entries(series)) { |
||
| 553 | let mangaList = seriesData.manga, |
||
| 554 | unreadCount = seriesData.unreadCount; |
||
| 555 | |||
| 556 | //region let table = ...; |
||
| 557 | let $table = $('<table/>', {'class': 'tablesorter-bootstrap tracker-table', 'style': (seriesStub === 'reading') ? '' : 'display : none', 'data-list': seriesStub, 'data-unread': unreadCount}).append( |
||
| 558 | $('<thead/>').append( |
||
| 559 | $('<tr/>').append( |
||
| 560 | $('<th/>', {class: 'header read'})).append( |
||
| 561 | $('<th/>', {class: 'header read'}).append( |
||
| 562 | $('<div/>', {class: 'tablesorter-header-inner', text: 'Series '+(unreadCount > 0 ? `(${unreadCount})` : '')}) |
||
| 563 | )).append( |
||
| 564 | $('<th/>', {class: 'header read'}).append( |
||
| 565 | $('<div/>', {class: 'tablesorter-header-inner', text: 'My Status'}) |
||
| 566 | )).append( |
||
| 567 | $('<th/>', {class: 'header read'}).append( |
||
| 568 | $('<div/>', {class: 'tablesorter-header-inner', text: 'Latest Release'}) |
||
| 569 | )).append( |
||
| 570 | $('<th/>', {'data-sorter': 'false'}).append( |
||
| 571 | $('<i/>', {class: 'fa fa-spinner fa-spin'}) |
||
| 572 | ) |
||
| 573 | ) |
||
| 574 | ) |
||
| 575 | ); |
||
| 576 | //endregion |
||
| 577 | |||
| 578 | //FIXME: Closure compiler toggled off. |
||
| 579 | |||
| 580 | let $tbody = $('<tbody/>'); |
||
| 581 | |||
| 582 | mangaList.forEach(manga => { // jshint ignore:line |
||
| 583 | let tr = generateRow(manga); |
||
| 584 | $tbody.append(tr); |
||
| 585 | }); |
||
| 586 | $table.append($tbody); |
||
| 587 | |||
| 588 | $table.appendTo('#list-container'); |
||
| 589 | } |
||
| 590 | this.$tables = $('.tracker-table'); //Reset cache. |
||
| 591 | this.setupListEvents(); |
||
| 592 | |||
| 593 | /** |
||
| 594 | * @param {Object} manga |
||
| 595 | * @param {String} manga.id |
||
| 596 | * @param {Object} manga.generated_current_data |
||
| 597 | * @param {Object} manga.generated_current_data.url |
||
| 598 | * @param {Object} manga.generated_current_data.number |
||
| 599 | * @param {Object} manga.generated_latest_data |
||
| 600 | * @param {Object} manga.generated_latest_data.url |
||
| 601 | * @param {Object} manga.generated_latest_data.number |
||
| 602 | * @param {Object|null} manga.generated_ignore_data |
||
| 603 | * @param {Object|null} manga.generated_ignore_data.url |
||
| 604 | * @param {Object|null} manga.generated_ignore_data.number |
||
| 605 | * @param {String} manga.full_title_url |
||
| 606 | * @param {Number} manga.new_chapter_exists |
||
| 607 | * @param {String} manga.tag_list |
||
| 608 | * @param {Boolean} manga.has_tags |
||
| 609 | * @param {String} manga.mal_id |
||
| 610 | * @param {String} manga.mal_type |
||
| 611 | * @param {String} manga.last_updated |
||
| 612 | * @param {Object} manga.title_data |
||
| 613 | * @param {String} manga.title_data.id |
||
| 614 | * @param {String} manga.title_data.title |
||
| 615 | * @param {String} manga.title_data.title_url |
||
| 616 | * @param {String} manga.title_data.latest_chapter |
||
| 617 | * @param {String} manga.title_data.current_chapter |
||
| 618 | * @param {String|null} manga.title_data.ignore_chapter |
||
| 619 | * @param {String} manga.title_data.last_updated |
||
| 620 | * @param {String} manga.title_data.time_class |
||
| 621 | * @param {Number} manga.title_data.status |
||
| 622 | * @param {Number} manga.title_data.failed_checks |
||
| 623 | * @param {Boolean} manga.title_data.active |
||
| 624 | * @param {Object} manga.site_data |
||
| 625 | * @param {String} manga.site_data.id |
||
| 626 | * @param {String} manga.site_data.site |
||
| 627 | * @param {String} manga.site_data.status |
||
| 628 | * @param {String} manga.mal_icon |
||
| 629 | */ |
||
| 630 | function generateRow(manga) { |
||
| 631 | let $mal_node = null; |
||
| 632 | if(manga.mal_id && manga.mal_type === 'chapter') { |
||
| 633 | $mal_node = $('<span/>') |
||
| 634 | .append(document.createTextNode('(')) |
||
| 635 | .append($('<small/>', {text: (manga.mal_id !== '0' ? manga.mal_id : 'none')})) |
||
| 636 | .append(document.createTextNode(')')); |
||
| 637 | } |
||
| 638 | |||
| 639 | //region let tr = ...; |
||
| 640 | let $tr = $('<tr/>', {'data-id': manga.id}).append( |
||
| 641 | $('<td/>') |
||
| 642 | .append($('<span/>', {hidden: true, text: manga.new_chapter_exists})) |
||
| 643 | .append($('<input/>', {type: 'checkbox', name: 'check'})) |
||
| 644 | ).append( |
||
| 645 | $('<td/>') |
||
| 646 | .append( |
||
| 647 | $('<div/>', {class: 'row-icons'}) |
||
| 648 | .append($('<i/>', {class: `sprite-time ${manga.title_data.time_class}`, title: manga.last_updated})) |
||
| 649 | .append($('<i/>', {class: `sprite-site sprite-${manga.site_data.site.replace(/\./g, '-')}`, title: manga.site_data.site})) |
||
| 650 | .append(manga.mal_icon) |
||
| 651 | ) |
||
| 652 | .append($('<a/>', {href: manga.full_title_url, rel: 'nofollow', class: 'title', 'data-title': decodeEntities(manga.title_data.title_url), target: '_blank', text: decodeEntities(manga.title_data.title)})) |
||
| 653 | |||
| 654 | .append($('<small/>', {class: 'toggle-info pull-right text-muted', text: 'More info'})) |
||
| 655 | .append($('<div/>', {class: 'more-info'}).append( |
||
| 656 | $('<small/>') |
||
| 657 | .append($('<a/>', {href: `/history/${manga.title_data.id}`, text: 'History'})) |
||
| 658 | .append(document.createTextNode(' | ')) |
||
| 659 | .append($('<a/>', {href: '#', class: 'set-mal-id', 'data-mal-id': manga.mal_id, 'data-mal-type': manga.mal_type, text: 'Set MAL ID'})) |
||
| 660 | .append($mal_node) |
||
| 661 | |||
| 662 | .append(document.createTextNode(' | Tags (')) |
||
| 663 | .append($('<a/>', {href: '#', class: 'edit-tags small', text: 'Edit'})) |
||
| 664 | .append(document.createTextNode('): ')) |
||
| 665 | .append( |
||
| 666 | $('<span/>', {class: 'text-lowercase tag-list', }) |
||
| 667 | .append(manga.has_tags ? (manga.tag_list.split(',').map((e) => { return $('<i/>', {class: 'tag', text: e}).get(0); })) : document.createTextNode('none'))) |
||
| 668 | |||
| 669 | .append( |
||
| 670 | $('<div/>', {class: 'input-group tag-edit', hidden: true}) |
||
| 671 | .append($('<input/>', {type: 'text', class: 'form-control', placeholder: 'tag1,tag2,tag3', maxlength: 255, pattern: '[a-z0-9-_,]{0,255}', value: manga.tag_list})) |
||
| 672 | .append( |
||
| 673 | $('<span/>', {class: 'input-group-btn'}) |
||
| 674 | .append($('<button/>', {class: 'btn btn-default', type: 'button', text: 'Save'})))))) |
||
| 675 | ).append( |
||
| 676 | $('<td/>', {'data-updated-at': manga.last_updated}) |
||
| 677 | .append($('<a/>', {class: 'chp-release current', href: manga.generated_current_data.url, rel: 'nofollow', target: '_blank', text: decodeEntities(manga.generated_current_data.number)})) |
||
| 678 | .append(manga.title_data.ignore_chapter ? $('<span/>', {class: 'hidden-chapter', title: 'The latest chapter was marked as ignored.', text: manga.generated_ignore_data.number}) : null) |
||
| 679 | ).append( |
||
| 680 | $('<td/>') |
||
| 681 | .append( |
||
| 682 | manga.generated_latest_data.number !== 'No chapters found' ? |
||
| 683 | $('<a/>', {class: 'chp-release latest', href: manga.generated_latest_data.url, rel: 'nofollow', 'data-chapter': manga.title_data.latest_chapter, target: '_blank', text: decodeEntities(manga.generated_latest_data.number)}) |
||
| 684 | : |
||
| 685 | $('<i/>', {title: 'Title page still appears to exist, but chapters have been removed. This is usually due to DMCA.', text: 'No chapters found'}) |
||
| 686 | ) |
||
| 687 | ).append( |
||
| 688 | $('<td/>') |
||
| 689 | .append( |
||
| 690 | manga.site_data.status === 'disabled' ? |
||
| 691 | $('<i/>', {class: 'fa fa-exclamation-triangle', 'aria-hidden': 'true', style: 'color: red', title: `Tracking has been disabled for this series as the site '${manga.site_data.site}' is disabled`}) |
||
| 692 | : |
||
| 693 | null |
||
| 694 | ) |
||
| 695 | .append( |
||
| 696 | manga.new_chapter_exists === 0 ? |
||
| 697 | $('<div/>', {class: 'row-icons'}) |
||
| 698 | .append( |
||
| 699 | $('<span/>', {class: 'list-icon ignore-latest', title: 'Ignore latest chapter. Useful when latest chapter isn\'t actually the latest chapter.'}) |
||
| 700 | .append($('<i/>', {class:' fa fa-bell-slash', 'aria-hidden': 'true'})) |
||
| 701 | ) |
||
| 702 | .append( |
||
| 703 | $('<span/>', {class: 'list-icon update-read', title: 'I\'ve read the latest chapter!'}) |
||
| 704 | .append($('<i/>', {class: 'fa fa-refresh', 'aria-hidden': 'true'})) |
||
| 705 | ) |
||
| 706 | : |
||
| 707 | null |
||
| 708 | ) |
||
| 709 | ); |
||
| 710 | //endregion |
||
| 711 | if(manga.site_data.status === 'disabled') { |
||
| 712 | $tr.addClass('bg-danger'); |
||
| 713 | } |
||
| 714 | else if(manga.title_data.status === 255) { |
||
| 715 | $tr.addClass('bg-danger'); |
||
| 716 | $tr.attr('title', 'This title is no longer being updated as it has been marked as deleted/ignored.'); |
||
| 717 | } |
||
| 718 | else if(manga.title_data.failed_checks >= 5) { |
||
| 719 | $tr.addClass('bg-danger'); |
||
| 720 | $tr.attr('title', 'The last 5+ updates for this title have failed, as such it may not be completely up to date.'); |
||
| 721 | } |
||
| 722 | else if(manga.title_data.failed_checks > 0) { |
||
| 723 | $tr.addClass('bg-warning'); |
||
| 724 | $tr.attr('title', 'The last update for this title failed, as such it may not be completely up to date.'); |
||
| 725 | } |
||
| 726 | |||
| 727 | return $tr; |
||
| 728 | } |
||
| 729 | } |
||
| 730 | setupListEvents() { |
||
| 731 | //This makes it easier to press the row checkbox. |
||
| 732 | this.$tables.find('> tbody > tr > td:nth-of-type(1)').on('click', function (e) { |
||
| 733 | if(!$(e.target).is('input')) { |
||
| 734 | let $checkbox = $(this).find('> input[type=checkbox]'); |
||
| 735 | $($checkbox).prop('checked', !$checkbox.prop('checked')); |
||
| 736 | } |
||
| 737 | }); |
||
| 738 | |||
| 739 | //Requires user confirm to change page if any boxes are checked |
||
| 740 | $('input[name=check]').on('change', function() { |
||
| 741 | if(window.onbeforeunload === null) { |
||
| 742 | window.onbeforeunload = function (e) { |
||
| 743 | e = e || window.event; |
||
| 744 | |||
| 745 | let dialogText = 'You appear to have some checked titles.\nDo you still wish to continue?'; |
||
| 746 | // For IE and Firefox prior to version 4 |
||
| 747 | if (e) { e.returnValue = dialogText; } |
||
| 748 | |||
| 749 | // For others, except Chrome (https://bugs.chromium.org/p/chromium/issues/detail?id=587940) |
||
| 750 | return dialogText; |
||
| 751 | }; |
||
| 752 | } else if($('.tracker-table:visible').find('tr:has(td input[type=checkbox]:checked)').length === 0) { |
||
| 753 | window.onbeforeunload = null; |
||
| 754 | } |
||
| 755 | }); |
||
| 756 | |||
| 757 | //This shows/hides the row info row. |
||
| 758 | $('.toggle-info').on('click', function(e) { |
||
| 759 | e.preventDefault(); |
||
| 760 | |||
| 761 | $(this).find('+ .more-info').toggle(); |
||
| 762 | if($(this).text() === 'More info') { |
||
| 763 | $(this).text('Hide info'); |
||
| 764 | } else { |
||
| 765 | $(this).text('More info'); |
||
| 766 | |||
| 767 | //Hide input when hiding info |
||
| 768 | $(this).closest('tr').find('.tag-edit').attr('hidden', true); |
||
| 769 | } |
||
| 770 | }); |
||
| 771 | |||
| 772 | //Set MAL ID |
||
| 773 | $('.set-mal-id').on('click', function(e) { |
||
| 774 | e.preventDefault(); |
||
| 775 | |||
| 776 | let _this = this, |
||
| 777 | current_mal_id = $(this).attr('data-mal-id'); |
||
| 778 | |||
| 779 | //If trackr.moe already has it's own MAL id for the series, ask if the user wants to override it (if they haven't already). |
||
| 780 | if($(this).attr('data-mal-type') === 'title' && $(this).attr('data-mal-id') && !confirm('A MAL ID already exists for this series on our backend.\n Are you sure you want to override it?')) { return; } |
||
| 781 | |||
| 782 | let new_mal_id = prompt('MAL ID:', current_mal_id); |
||
| 783 | if(/^([0-9]+|none)?$/.test(new_mal_id)) { |
||
| 784 | if(/^[0-9]+$/.test(new_mal_id)) { new_mal_id = parseInt(new_mal_id); } //Stops people submitting multiple 0s |
||
| 785 | |||
| 786 | let tr = $(this).closest('tr'), |
||
| 787 | td = tr.find('td:eq(1)'), |
||
| 788 | table = $(this).closest('table'), |
||
| 789 | id = tr.attr('data-id'), |
||
| 790 | icon_link = $(td).find('.sprite-myanimelist-net').parent(), |
||
| 791 | iconN_link = $(td).find('.sprite-myanimelist-net-none').parent(), |
||
| 792 | id_text = $(this).find('+ span'), |
||
| 793 | deferred = $.Deferred(); |
||
| 794 | |||
| 795 | if(new_mal_id !== '' && new_mal_id !== 'none' && new_mal_id !== 0) { |
||
| 796 | set_mal_id(id, new_mal_id, () => { |
||
| 797 | $(iconN_link).remove(); //Make sure to remove MAL none icon when changing ID |
||
| 798 | if(icon_link.length) { |
||
| 799 | //icon exists, just change link |
||
| 800 | $(icon_link).attr('href', 'https://myanimelist.net/manga/'+new_mal_id); |
||
| 801 | $(icon_link).find('.sprite-myanimelist-net').attr('title', new_mal_id); |
||
| 802 | } else { |
||
| 803 | $($('<a/>', {href: 'https://myanimelist.net/manga/'+new_mal_id, class: 'mal-link'}).append( |
||
| 804 | $('<i/>', {class: 'sprite-site sprite-myanimelist-net', title: new_mal_id}) |
||
| 805 | )).prepend(' ').insertAfter(td.find('.sprite-site')); |
||
| 806 | } |
||
| 807 | |||
| 808 | set_id_text($(_this), id_text, new_mal_id); |
||
| 809 | |||
| 810 | deferred.resolve(); |
||
| 811 | }); |
||
| 812 | } else { |
||
| 813 | if(new_mal_id === 'none' || new_mal_id === 0) { |
||
| 814 | set_mal_id(id, '0', () => { |
||
| 815 | icon_link.remove(); |
||
| 816 | iconN_link.remove(); |
||
| 817 | |||
| 818 | $($('<a/>', {class: 'mal-link'}).append( |
||
| 819 | $('<i/>', {class: 'sprite-site sprite-myanimelist-net-none', title: new_mal_id}) |
||
| 820 | )).prepend(' ').insertAfter(td.find('.sprite-site')); |
||
| 821 | |||
| 822 | set_id_text($(_this), id_text, 'none'); |
||
| 823 | |||
| 824 | deferred.resolve(); |
||
| 825 | }); |
||
| 826 | } else { |
||
| 827 | set_mal_id(id, null, () => { |
||
| 828 | icon_link.remove(); |
||
| 829 | iconN_link.remove(); |
||
| 830 | id_text.remove(); |
||
| 831 | |||
| 832 | deferred.resolve(); |
||
| 833 | }); |
||
| 834 | } |
||
| 835 | } |
||
| 836 | |||
| 837 | deferred.done(() => { |
||
| 838 | $(this).attr('data-mal-id', new_mal_id); |
||
| 839 | table.trigger('updateCell', [td[0], false, null]); |
||
| 840 | }); |
||
| 841 | } else if (new_mal_id === null) { |
||
| 842 | //input cancelled, do nothing |
||
| 843 | } else { |
||
| 844 | alert('MAL ID can only contain numbers.'); |
||
| 845 | } |
||
| 846 | |||
| 847 | function set_id_text(_this, id_text, text) { |
||
| 848 | text = (text !== '0' ? text : 'none'); |
||
| 849 | if(id_text.length) { |
||
| 850 | id_text.find('small').text(text); |
||
| 851 | } else { |
||
| 852 | $('<span/>').append( |
||
| 853 | $('<small/>', {text: text}) |
||
| 854 | ).prepend(' (').append(')').insertAfter(_this); |
||
| 855 | } |
||
| 856 | } |
||
| 857 | |||
| 858 | function set_mal_id(id, mal_id, successCallback) { |
||
| 859 | successCallback = successCallback || function(){}; |
||
| 860 | |||
| 861 | let postData = { |
||
| 862 | 'id' : id, |
||
| 863 | 'mal_id' : mal_id |
||
| 864 | }; |
||
| 865 | $.post(base_url + 'ajax/set_mal_id', postData, () => { |
||
| 866 | successCallback(); |
||
| 867 | }).fail((jqXHR, textStatus, errorThrown) => { |
||
| 868 | _handleAjaxError(jqXHR, textStatus, errorThrown); |
||
| 869 | }); |
||
| 870 | } |
||
| 871 | }); |
||
| 872 | |||
| 873 | this.setupTagEditor(); |
||
| 874 | |||
| 875 | //Ignore latest chapter |
||
| 876 | $('.ignore-latest').on('click', function() { |
||
| 877 | let row = $(this).closest('tr'), |
||
| 878 | table = $(this).closest('table'), |
||
| 879 | chapter_id = $(row).attr('data-id'), |
||
| 880 | current_chapter = $(row).find('.current'), |
||
| 881 | latest_chapter = $(row).find('.latest'); |
||
| 882 | |||
| 883 | if(confirm('Ignore latest chapter?')) { |
||
| 884 | let postData = { |
||
| 885 | id : chapter_id, |
||
| 886 | chapter : latest_chapter.attr('data-chapter') |
||
| 887 | }; |
||
| 888 | $.post(base_url + 'ajax/ignore_inline', postData, () => { |
||
| 889 | $(current_chapter).parent().append( |
||
| 890 | $('<span/>', {class: 'hidden-chapter', title: 'This latest chapter was marked as ignored.', text: $(latest_chapter).text()}) |
||
| 891 | ); |
||
| 892 | |||
| 893 | updateUnread(table, row); |
||
| 894 | }).fail((jqXHR, textStatus, errorThrown) => { |
||
| 895 | _handleAjaxError(jqXHR, textStatus, errorThrown); |
||
| 896 | }); |
||
| 897 | } |
||
| 898 | }); |
||
| 899 | |||
| 900 | //Update latest chapter (via "I've read the latest chapter") |
||
| 901 | $('.update-read').on('click', function(e, data) { |
||
| 902 | let row = $(this).closest('tr'), |
||
| 903 | table = $(this).closest('table'), |
||
| 904 | chapter_id = $(row).attr('data-id'), |
||
| 905 | current_chapter = $(row).find('.current'), |
||
| 906 | latest_chapter = $(row).find('.latest'); |
||
| 907 | |||
| 908 | if (!(data && data.isUserscript)) { |
||
| 909 | let postData = { |
||
| 910 | id : chapter_id, |
||
| 911 | chapter: latest_chapter.attr('data-chapter') |
||
| 912 | }; |
||
| 913 | $.post(base_url + 'ajax/update_inline', postData, () => { |
||
| 914 | $(current_chapter) |
||
| 915 | .attr('href', $(latest_chapter).attr('href')) |
||
| 916 | .text($(latest_chapter).text()); |
||
| 917 | |||
| 918 | updateUnread(table, row); |
||
| 919 | }).fail((jqXHR, textStatus, errorThrown) => { |
||
| 920 | _handleAjaxError(jqXHR, textStatus, errorThrown); |
||
| 921 | }); |
||
| 922 | } else { |
||
| 923 | console.log('Userscript is updating table...'); |
||
| 924 | |||
| 925 | //Userscript handles updating current_chapter url/text. |
||
| 926 | |||
| 927 | if(data.isLatest) { |
||
| 928 | updateUnread(table, row); |
||
| 929 | } else { |
||
| 930 | let chapter_e = current_chapter.parent(); |
||
| 931 | |||
| 932 | //Update updated-at time for sorting purposes. |
||
| 933 | chapter_e.attr('data-updated-at', (new Date()).toISOString().replace(/^([0-9]+-[0-9]+-[0-9]+)T([0-9]+:[0-9]+:[0-9]+)\.[0-9]+Z$/, '$1 $2')); |
||
| 934 | table.trigger('updateCell', [chapter_e[0], false, null]); |
||
| 935 | } |
||
| 936 | } |
||
| 937 | }); |
||
| 938 | } |
||
| 939 | setupTagEditor() { |
||
| 940 | let _class = this; |
||
| 941 | //Toggle input on clicking "Edit" |
||
| 942 | $('.edit-tags').on('click', function(e) { |
||
| 943 | e.preventDefault(); |
||
| 944 | let editorEle = $(this).parent().find('.tag-edit'); |
||
| 945 | editorEle.attr('hidden', function(_, attr){ return !attr; }); |
||
| 946 | if(!editorEle[0].hasAttribute('hidden')) { |
||
| 947 | //NOTE: setTimeout is required here due to a chrome bug. |
||
| 948 | setTimeout(function(){ |
||
| 949 | let input = editorEle.find('> input'); |
||
| 950 | input.focus(); |
||
| 951 | |||
| 952 | //Resetting value to force pointer to end of line |
||
| 953 | //SEE: https://stackoverflow.com/a/8631903 |
||
| 954 | let tmp_val = input.val(); |
||
| 955 | input.val(''); |
||
| 956 | input.val(tmp_val); |
||
| 957 | }, 1); |
||
| 958 | } |
||
| 959 | }); |
||
| 960 | |||
| 961 | |||
| 962 | //Simulate "Save" click on enter press. |
||
| 963 | $('.tag-edit input').on('keypress', function(e) { |
||
| 964 | if(e.which === /* enter */ 13) { |
||
| 965 | $(this).closest('.tag-edit').find('[type=button]').click(); |
||
| 966 | } |
||
| 967 | }); |
||
| 968 | |||
| 969 | //Submit tags |
||
| 970 | $('.tag-edit [type=button]').on('click', function() { |
||
| 971 | let _this = this; |
||
| 972 | //CHECK: We would use jQuery.validate here but I don't think it works without an actual form. |
||
| 973 | let input = $(this).closest('.tag-edit').find('input'), |
||
| 974 | tag_list = input.val().toString().trim().replace(/,,/g, ','), |
||
| 975 | id = $(this).closest('tr').attr('data-id'), |
||
| 976 | table = $(this).closest('table'), |
||
| 977 | td = $(this).closest('td'); |
||
| 978 | |||
| 979 | //Validation |
||
| 980 | _class.validateTagList(tag_list, (tag_list_new) => { |
||
| 981 | let postData = { |
||
| 982 | 'id' : id, |
||
| 983 | 'tag_string' : tag_list_new |
||
| 984 | }; |
||
| 985 | $.post(base_url + 'ajax/tag_update', postData, () => { |
||
| 986 | $(input).val(tag_list_new); |
||
| 987 | |||
| 988 | let $tag_list = $(_this).closest('.more-info').find('.tag-list'); |
||
| 989 | if(!tag_list_new) { |
||
| 990 | $tag_list.text('none'); |
||
| 991 | } else { |
||
| 992 | let tagArr = tag_list_new.split(',').map((e/*, i*/) => { |
||
| 993 | return $('<i/>', {class: 'tag', text: e}); |
||
| 994 | }); |
||
| 995 | $tag_list.html(tagArr); |
||
| 996 | } |
||
| 997 | |||
| 998 | table.trigger('updateCell', [td[0], false, null]); |
||
| 999 | |||
| 1000 | $(_this).closest('.tag-edit').attr('hidden', function(_, attr){ return !attr; }); |
||
| 1001 | }).fail((jqXHR, textStatus, errorThrown) => { |
||
| 1002 | _handleAjaxError(jqXHR, textStatus, errorThrown); |
||
| 1003 | }); |
||
| 1004 | }); |
||
| 1005 | }); |
||
| 1006 | } |
||
| 1007 | validateTagList(tag_list, callback) { |
||
| 1008 | if(!$.isArray(tag_list)) { tag_list = tag_list.trim().replace(/,,/g, ','); } |
||
| 1009 | |||
| 1010 | if(/^[a-z0-9\-_,:]{0,255}$/.test(tag_list)) { |
||
| 1011 | let tag_array = Array.from(new Set(tag_list.split(','))).filter(function(n){ return n !== ''; }), |
||
| 1012 | tag_list_new = tag_array.join(','); |
||
| 1013 | if($.inArray('none', tag_array) === -1) { |
||
| 1014 | if((tag_list.match(/\bmal:(?:[0-9]+|none)\b/g) || []).length <= 1) { |
||
| 1015 | callback(tag_list_new); |
||
| 1016 | } else { |
||
| 1017 | alert('You can only use one MAL ID tag per series'); |
||
| 1018 | } |
||
| 1019 | } else { |
||
| 1020 | alert('"none" is a restricted tag.'); |
||
| 1021 | } |
||
| 1022 | } else { |
||
| 1023 | //Tag list is invalid. |
||
| 1024 | alert('Tags can only contain: lowercase a-z, 0-9, -, :, & _. They can also only have one MAL metatag.'); |
||
| 1025 | } |
||
| 1026 | } |
||
| 1027 | |||
| 1028 | startTablesorter() { |
||
| 1029 | //region Setup sorting methods & metatags. |
||
| 1030 | $.tablesorter.addParser( |
||
| 1031 | { |
||
| 1032 | id: 'updated-at', |
||
| 1033 | |||
| 1034 | is: function() { |
||
| 1035 | return false; // return false so this parser is not auto detected |
||
| 1036 | }, |
||
| 1037 | |||
| 1038 | format: function(s, table, cell/*, cellIndex*/) { |
||
| 1039 | return parseInt($(cell).attr('data-updated-at').replace(/[^0-9]+/g, '').toString()); |
||
| 1040 | }, |
||
| 1041 | |||
| 1042 | type: 'numeric' |
||
| 1043 | } |
||
| 1044 | ); |
||
| 1045 | |||
| 1046 | $.tablesorter.addParser( |
||
| 1047 | { |
||
| 1048 | id: 'latest', |
||
| 1049 | |||
| 1050 | is: function() { |
||
| 1051 | return false; // return false so this parser is not auto detected |
||
| 1052 | }, |
||
| 1053 | |||
| 1054 | format: function(s, table, cell/*, cellIndex*/) { |
||
| 1055 | return parseInt($(cell).closest('tr').find('td:eq(1) .sprite-time').attr('title').replace(/[^0-9]+/g, '').toString()); |
||
| 1056 | }, |
||
| 1057 | |||
| 1058 | type: 'numeric' |
||
| 1059 | } |
||
| 1060 | ); |
||
| 1061 | |||
| 1062 | /** |
||
| 1063 | * @return {boolean|null} |
||
| 1064 | */ |
||
| 1065 | $.tablesorter.filter.types.FindMalId = function( config, data ) { |
||
| 1066 | if(/^mal:(?:[0-9]+|any|none|notset|duplicates?)$/.test(data.iFilter)) { |
||
| 1067 | let searchID = data.iFilter.match(/^mal:([0-9]+|any|none|notset|duplicates?)$/)[1].toLowerCase(); |
||
| 1068 | if(searchID === 'duplicates') { searchID = 'duplicate'; } |
||
| 1069 | |||
| 1070 | let status = false; |
||
| 1071 | switch(searchID) { |
||
| 1072 | case 'any': |
||
| 1073 | status = (data.$row.find('> td:eq(1) i.sprite-myanimelist-net').length > 0); |
||
| 1074 | break; |
||
| 1075 | |||
| 1076 | case 'none': |
||
| 1077 | status = (data.$row.find('> td:eq(1) i.sprite-myanimelist-net-none').length > 0); |
||
| 1078 | break; |
||
| 1079 | |||
| 1080 | case 'notset': |
||
| 1081 | status = (data.$row.find('> td:eq(1) i[class*="sprite-myanimelist-net"]').length === 0); |
||
| 1082 | break; |
||
| 1083 | |||
| 1084 | case 'duplicate': |
||
| 1085 | if(data.$row.find('> td:eq(1) i.sprite-myanimelist-net').length > 0) { |
||
| 1086 | let malID = data.$row.find('> td:eq(1) i.sprite-myanimelist-net').attr('title'), |
||
| 1087 | $foundRows = data.$row.parent().find(`tr > td:nth-of-type(2) i.sprite-myanimelist-net[title=${malID}]`); |
||
| 1088 | |||
| 1089 | status = ($foundRows.length > 1); |
||
| 1090 | } |
||
| 1091 | break; |
||
| 1092 | |||
| 1093 | default: |
||
| 1094 | let currentID = data.$row.find('> td:eq(1) i.sprite-myanimelist-net').attr('title'); |
||
| 1095 | status = (searchID === currentID); |
||
| 1096 | break; |
||
| 1097 | } |
||
| 1098 | |||
| 1099 | return status; |
||
| 1100 | } |
||
| 1101 | return null; |
||
| 1102 | }; |
||
| 1103 | |||
| 1104 | /** |
||
| 1105 | * @return {boolean|null} |
||
| 1106 | */ |
||
| 1107 | $.tablesorter.filter.types.FindSite = function( config, data ) { |
||
| 1108 | if(/^site:[\w-.]+$/.test(data.iFilter)) { |
||
| 1109 | let searchSite = data.iFilter.match(/^site:([\w-.]+)$/i)[1].replace(/\./g, '-').toLowerCase(), |
||
| 1110 | currentSite = data.$row.find('> td:eq(1) .sprite-site').attr('class').split(' ')[1].substr(7); |
||
| 1111 | |||
| 1112 | return searchSite === currentSite; |
||
| 1113 | } |
||
| 1114 | return null; |
||
| 1115 | }; |
||
| 1116 | |||
| 1117 | /** |
||
| 1118 | * @return {boolean|null} |
||
| 1119 | */ |
||
| 1120 | $.tablesorter.filter.types.FindTag = function(config, data) { |
||
| 1121 | if(/^tag:[a-z0-9\-_:,]{1,255}$/.test(data.iFilter)) { |
||
| 1122 | let searchTagList = data.iFilter.match(/^tag:([a-z0-9\-_:,]{1,255})$/)[1], |
||
| 1123 | searchTagArray = searchTagList.split(','); |
||
| 1124 | |||
| 1125 | let rowTagArray = data.$row.find('td:eq(1) .tag-list .tag').map((i, e) => { |
||
| 1126 | return $(e).text(); |
||
| 1127 | }).toArray(); |
||
| 1128 | |||
| 1129 | return searchTagArray.every(tag => ($.inArray(tag, rowTagArray) !== -1)); |
||
| 1130 | } |
||
| 1131 | return null; |
||
| 1132 | }; |
||
| 1133 | /** |
||
| 1134 | * @return {boolean|null} |
||
| 1135 | */ |
||
| 1136 | $.tablesorter.filter.types.FindChecked = function( config, data ) { |
||
| 1137 | if(/^checked:(?:yes|no)$/.test(data.iFilter)) { |
||
| 1138 | let checked = data.iFilter.match(/^checked:(yes|no)$/)[1].toLowerCase(); |
||
| 1139 | |||
| 1140 | let status = data.$row.find('> td:eq(0) input').is(':checked'); |
||
| 1141 | if(checked === 'no') { status = !status; } |
||
| 1142 | |||
| 1143 | return status; |
||
| 1144 | } |
||
| 1145 | return null; |
||
| 1146 | }; |
||
| 1147 | |||
| 1148 | //The range filter uses "to" as a designator which can cause issues when searching. - SEE: #221 |
||
| 1149 | //FIXME: We should try and presserve the original filter and just remove to "to" designator. Same goes to the "and" designator for |
||
| 1150 | delete $.tablesorter.filter.types.range; |
||
| 1151 | //endregion |
||
| 1152 | |||
| 1153 | this.refreshTablesorter(); |
||
| 1154 | } |
||
| 1155 | refreshTablesorter() { |
||
| 1156 | this.$tables.trigger('destroy'); |
||
| 1157 | |||
| 1158 | this.$tables.tablesorter(this.tablesorterDefaults); |
||
| 1159 | } |
||
| 1160 | static getListSort(type, order) { |
||
| 1161 | let sortArr = []; |
||
| 1162 | |||
| 1163 | let sortOrder = (order === 'asc' ? 'a' : 'd'); |
||
| 1164 | switch(type) { |
||
| 1165 | case 'unread': |
||
| 1166 | sortArr = [[/* unread */ 0, 'a'], [/* title*/ 1, sortOrder]]; |
||
| 1167 | break; |
||
| 1168 | |||
| 1169 | case 'unread_latest': |
||
| 1170 | sortArr = [[/* unread */ 0, 'a'], [/* title*/ 3, sortOrder]]; |
||
| 1171 | break; |
||
| 1172 | |||
| 1173 | case 'alphabetical': |
||
| 1174 | sortArr = [[/* title */ 1, sortOrder]]; |
||
| 1175 | break; |
||
| 1176 | |||
| 1177 | case 'my_status': |
||
| 1178 | sortArr = [[/* unread */ 2, sortOrder]]; |
||
| 1179 | break; |
||
| 1180 | |||
| 1181 | case 'latest': |
||
| 1182 | sortArr = [[/* unread */ 3, sortOrder]]; |
||
| 1183 | break; |
||
| 1184 | |||
| 1185 | default: |
||
| 1186 | break; |
||
| 1187 | } |
||
| 1188 | |||
| 1189 | return sortArr; |
||
| 1190 | } |
||
| 1191 | } |
||
| 1192 | let App = new TrackrApp(); |
||
| 1193 | App.start(); |
||
| 1194 | }); |
||
| 1195 |
If JSHint finds too many errors in a file, it aborts checking altogether because it suspects a configuration issue.
Further Reading: