關於 web service, unity, blogger 等軟體工程筆記

Accupass 瀏覽擴充功能,使用 Javascript + TamperMonkey 自製自動加載頁面

Edit icon 沒有留言
Javascript
提示:發佈文章前發現其網站改版,以下腳本是針對舊版本的活動瀏覽

Accupass 活動通,是一個集結各式活動並販售活動票券網站(亦可參考另一個同類型的網站 KKTIX),自己蠻常在活動通上瀏覽活動,讓自己的下班生活能夠多點變化,接觸職場沒機會接觸的議題與活動。

但該網站的瀏覽模式用久會很惱人,那活動分頁模式非常難用,必須得用滑鼠去找下一頁的按鈕,重新捲動頁面到網頁頂端,才能看完新一頁的活動資訊,這是多麼消耗注意力啊。難道沒有辦法向 Pinterest 那樣的瀑布式瀏覽嗎?自動加載下一頁的活動資訊,只要一直按下 Space 鍵捲動頁面到頁面底部,持續加載活動資訊,就可以看完指定搜尋條件的活動列表。

因此寫了封信詢問 Accupass 團隊,有沒有機會可以改成如此的瀏覽模式,但獲得是如同罐頭訊息的回覆……。身為一個軟體工程師,既然沒人可以幫忙解決自己的痛點,那麼就自己嘗試做一個吧。從 Chrome developer console 中分析,活動通採用 Angular 前端框架,自己完全陌生的框架,真的不知道如何起手,不知道如何找到 binding 函數,如何將資料與 view binding 在一起呢?

想想先從自己熟悉 jQuery 開始吧,於是弄出以下的簡易 Script,能安裝在 TamperMonkey。TamperMonkey 是一個 Chrome 擴充套件,能管理瀏覽特定網頁時,加載執行已設定的用戶端腳本,腳本用來調整網頁內容,讓用戶瀏覽體驗更好甚至更方便。雖然做法上有點暴力,但至少有達到預先設想的目地。

// ==UserScript==
// @name         Accupass Infinite Scrolling
// @name:zh-TW   活動通 (Accupass) 無限捲頁 (自動加載下一頁)
// @namespace    http://tampermonkey.net/
// @version      0.4
// @description  Load next event page automatically when scroll to bottom of the page on [Accupass](https://www.accupass.com/).
// @description:zh-TW  在[活動通]((https://www.accupass.com/))瀏覽活動時,當頁面捲到頁面底部,將會自動加載下一頁活動內容,而不用手動點擊分頁按鈕
// @author       Siyuan
// @match        https://old.accupass.com/search/*
// @grant        none
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/URI.js/1.18.10/URI.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment.min.js
// ==/UserScript==
(function($, undefined) {
   $(function() {

      var loading = false;
      var initNexturl = false;
      var nexturl = null;

      $(document).ready(function() {
         // repack pushState for clearing loading page when changed search arguments
         (function(history) {
            var pushState = history.pushState;
            history.pushState = function(state) {
               initNexturl = false;
               nexturl = null;
               return pushState.apply(history, arguments);
            };
         })(window.history);

         $(window).scroll(function() {
            if ($(window).scrollTop() + $(window).height() > $(document).height() - 600) {
               doLoadNext();
            }
         });

      });

      function fixAuccpassStupidErr(uri) {
         // Fixed date error cause by Accupass .../search/changeconditions/r/0/0/6/0/4/1/20170727/20170713?q= to /search/changeconditions/r/0/0/6/0/4/1/20170713/20170727?q=
         var u = new URI(uri);
         var d = u.pathname().split('/');
         if (d.length >= 2) {
            var d1 = moment(d[d.length - 2], 'YYYYMMDD');
            var d2 = moment(d[d.length - 1], 'YYYYMMDD');
            if (d1.isValid() && d2.isValid()) {
               if (!d1.isBefore(d2)) {
                  d[d.length - 2] = d2.format('YYYYMMDD');
                  d[d.length - 1] = d1.format('YYYYMMDD');
                  u.pathname(d.join('/'));
               }
            }
         }
         return u.toString();
      }

      function getNextPageUrl(dom) {
         if (!dom) {
            dom = $(document);
         }

         var a = dom.find("ul.pagination li.active");
         if (a.length) {
            a = a.next();
            if (a.length) {
               var h = a.find('a').attr('href');
               if (h.length) {
                  if (h.length >= 7) {
                     if (h.indexOf('/search/') === 0) {
                        h = h.substring(0, 7) + '/changeconditions' + h.substring(7);
                        return fixAuccpassStupidErr(h);
                     }
                  }
               }
            }
            return '';
         }

         return null;
      }

      function doLoadNext() {
         if (loading)
            return;

         if (!initNexturl) {
            nexturl = getNextPageUrl();
            initNexturl = nexturl !== null;
            if (initNexturl) {
               console.log("Next: " + nexturl);
            }
         }

         if (!nexturl)
            return;

         loading = true;

         // Not sure how to bind data within angular view, so use string replace to build final HTML here
         var previousSibling = $('section div[ga-hover="Page"]');
         $loader = $('<div class="clearfix"/><div class="row" style="text-align: center;">Loading...<img src="https://i0.wp.com/cdnjs.cloudflare.com/ajax/libs/galleriffic/2.0.1/css/loader.gif?resize=24%2C24"></div>');
         $loader.insertBefore(previousSibling);

         $.ajax({
            url: nexturl,
         }).always(function() {
            loading = false;
            $loader.remove();
         }).done(function(html) {
            var dom = $($.parseHTML(html));
            var template = `
     <div class="apcss-activity-card-header">
        <a class="apcss-activity-card-image" href="/event/register/JSON:eventIdNumber" target="_self">
            <img alt="JSON:name" src="JSON:photoUrl">
        </a>
        <span class="apcss-activity-pageview">
            <i class="icon-eye-open"></i>JSON:pageview
        </span>
    </div>
    <div class="apcss-activity-card-body">
        <a href="/event/register/JSON:eventIdNumber" target="_self">
            <h3 class="apcss-activity-card-title">JSON:name</h3>
        </a>
        <p class="apcss-activity-card-date">JSON:fullDateTimeStr</p>
        JSON:summary
    </div>
    <div class="apcss-activity-card-footer">
        <div class="row">
            <div class="col-xs-6">
                <i class="icon-heart"></i>
                <span class="apcss-activity-card-like"> JSON:likeCount Likes</span>
            </div>
            <div class="col-xs-6">JSON:StateButton
            </div>
        </div>
    </div>
`;

            dom.find('div[event-card]').each(function() {
               var raw = $(this).attr('event-row');
               var e = JSON.parse(raw);

               var bh = template;
               var buttonTemplate = '';
               if (e.remainingTicket > 0) {
                  if (e.eventCardStatus == 2) {
                     buttonTemplate = '<a class="apcss-btn apcss-btn-block activity-card-status-ready" target="_self" href="/event/register/JSON:eventIdNumber">On Sale (JSON:remainingTicket)</a>';
                  } else {
                     buttonTemplate = '<a class="apcss-btn apcss-btn-block activity-card-status-hot" target="_self" href="/event/register/JSON:eventIdNumber">On Sale (JSON:remainingTicket)</a>';
                  }
               } else {
                  buttonTemplate = '<a class="apcss-btn apcss-btn-block activity-card-status-end" target="_self" href="/event/register/JSON:eventIdNumber">Sold Out</a>';
               }

               bh = bh.replace(/JSON:StateButton/g, buttonTemplate);
               bh = bh.replace(/JSON:remainingTicket/g, e.remainingTicket);
               bh = bh.replace(/JSON:eventIdNumber/g, e.eventIdNumber);
               bh = bh.replace(/JSON:name/g, e.name);
               bh = bh.replace(/JSON:photoUrl/g, e.photoUrl);
               bh = bh.replace(/JSON:pageview/g, e.pageview);
               bh = bh.replace(/JSON:fullDateTimeStr/g, e.fullDateTimeStr);
               bh = bh.replace(/JSON:summary/g, e.summary);
               bh = bh.replace(/JSON:likeCount/g, e.likeCount);

               $(this).addClass('apcss-activity-card');
               $(this).append($(bh));
               $(this).parent().insertBefore(previousSibling);

            });

            nexturl = getNextPageUrl(dom);

            if (nexturl !== null && nexturl.length <= 0) {
               console.log("End");
               $end = $('<div class="clearfix"/><div class="row" style="text-align: center;">--End--</div>');
               $end.insertBefore(previousSibling);
            } else {
               console.log("Next: " + nexturl);
            }
         });
      }
   });
})(window.jQuery.noConflict(true));

該程式碼腳本也上傳到 Github,並將其加入到 Greasyfork,該網站有許多 TamperMonkey 全球開發者貢獻的腳本。可點選以下連結前往安裝或是查看最新的程式碼:

Greasy Fork
Github

沒有留言: