Infinite scroller

2021. 1. 6. 오전 11:17:30

As of 25.01.2021. See апошняя версія.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name        Infinite scroller
// @namespace   Violentmonkey Scripts
// @match       https://*.arca.live/*
// @grant       none
// @version     1.0.2
// @author      awk
// @description 2021. 1. 6. 오전 11:17:30
// ==/UserScript==

// bounce-back element size
const bounceBackBuffer = 96;

// last-checked page
let scrolledPage = 1;
let isLoading = false;

// from Arca Refresher@LeKAKiD v2.6.8
function getTimeStr(datetime) {
  const date = new Date(datetime);
  let hh = date.getHours();
  let mm = date.getMinutes();

  if (hh.toString().length == 1) {
    hh = `0${hh}`;
  }

  if (mm.toString().length == 1) {
    mm = `0${mm}`;
  }

  return `${hh}:${mm}`;
}

function in24(datetime) {
  const target = new Date(datetime);
  const criteria = new Date();
  criteria.setHours(criteria.getHours() - 24);
  if (target > criteria) return true;
  return false;
}

const bounceBack = () => {
  if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
    scrollBy({
      'top': -bounceBackBuffer,
      'left': 0,
      'behavior': 'smooth'
    });
  }
};

const getArticleId = elem => ~~elem.href.match(/\/b\/[^/]+\/(\d+)/)[1];

const loadMore = async () => {
  if(isLoading) return;
  isLoading = true;
  
  // true if scrolledPage value is reached to unread page
  let reached = false;
  
  const currentArticles = [...document.querySelectorAll('.vrow')];
  const currentLastArticleId = getArticleId(currentArticles[currentArticles.length - 1]);
  
  // determine if current page doesn't include any article
  if(!currentLastArticleId) {
    isLoading = false;
    return;
  }
  
  while(!reached) {
    
    ++scrolledPage;
    // get next-page url
    const nextPage = (() => {
      // determine if current url includes page information
      if(location.href.match(/[&?]p=(\d+)/)) {
        return location.href.replace(/[&?]p=\d+/, pageString => {
          return pageString.replace(/\d+/, scrolledPage);
        });
      }
      // determine if current url includes query string
      else if(location.href.match(/\?.+?=.+?/)){
        return `${location.href}&p=${scrolledPage}`;
      }
      // it might be first-page of the board
      else {
        return `${location.href}?p=${scrolledPage}`;
      }
    })();
    
    console.log(nextPage);

    // fetch and parse next-page DOM
    const nextPageDOM = await fetch(nextPage)
      .then(res => res.text())
      .then(pageText => new DOMParser().parseFromString(pageText, 'text/html'));

    const nextPageLastArticle = nextPageDOM.querySelector('.vrow:last-child');
    // determine if reached to end
    if(!nextPageLastArticle || nextPageLastArticle.classList.length != 1) {
      // reached to end
      bounceBack();
      break;
    }
    
    // determine if reached to unread page
    const nextPageLastArticleId = getArticleId(nextPageLastArticle);
    if(currentLastArticleId > nextPageLastArticleId) {
      // filter unread articles
      const unreadArticles = [...nextPageDOM.querySelectorAll('.vrow')].filter(article => {
        // filter out notices
        if(article.classList.length != 1) return false;
        return getArticleId(article) < currentLastArticleId;
      });
      
      // attach unread articles with applying <time> js
      const articleList = document.querySelector('.list-table');
      unreadArticles.forEach(article => {
        const time = article.querySelector('time');

        if (time && in24(time.dateTime)) {
          time.innerText = getTimeStr(time.dateTime);
        }
        articleList.appendChild(article)
      });
      break;
    }
  }
  isLoading = false;
  return;
}

window.addEventListener('load', e => {
  // load current page
  const pageMatch = location.href.match(/[&?]p=(\d+)/);
  console.log(location.href);
  scrolledPage = pageMatch ? ~~pageMatch[1] : 1;
  
  // attach bounce-back element
  const bounceBackElement = document.createElement('div');
  bounceBackElement.style.width = 'inherit';
  bounceBackElement.style.height = `${bounceBackBuffer}px`;
  // center-align text
  bounceBackElement.style.textAlign = 'center';
  bounceBackElement.style.lineHeight = `${bounceBackBuffer}px`;
  // element color
  bounceBackElement.style.backgroundColor = 'grey';
  bounceBackElement.style.color = 'white';
  // element text
  bounceBackElement.innerText = 'Loading next page...';
  bounceBackElement.fontWeight = 'bold';
  document.body.appendChild(bounceBackElement);
  
  // apply smooth-scroll for bounce-back
  document.body.style.scrollBehavior = 'smooth';
});

window.addEventListener('scroll', e => {
  if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
    loadMore();
  }
});