F95 Mobile Upgrade

Improves mobile experience

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

作者のサイトでサポートを受ける。または、このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name        F95 Mobile Upgrade
// @namespace   1330126-edexal
// @match       *://f95zone.to/*
// @grant       none
// @icon        https://external-content.duckduckgo.com/ip3/f95zone.to.ico
// @license     Unlicense
// @version     1.8.5
// @author      Edexal
// @description Improves mobile experience
// @homepageURL https://sleazyfork.org/en/scripts/546346-f95-mobile-upgrade
// @supportURL  https://github.com/Edexaal/scripts/issues
// @require     https://cdn.jsdelivr.net/gh/Edexaal/scripts@20abbf4a49807e7d11a081eb3a8573d0cab83c1f/_lib/utility.js
// ==/UserScript==
(async () => {
  /*NOTE: F95 uses FontAwesome v5.15.4*/
  const SELECTOR = {
    likeBtns: 'a.reaction.actionBar-action',
    reactionBtns: 'button.reaction.actionBar-action',
    thread: '.p-body-main',
    latestUpdate: '#latest-page_main-wrap',
    latestFilterDrawer: '#latest-page_filter-wrap',
  };
  let REACTION_BAR;
  const MAX_SCREEN_WIDTH = 480;
  const SWIPE_DOWN_CONFIG = {
    isValidSwipeDuration: false,
    lastDownYPos: 0,
    //Time (ms)
    galleryEventDelay: 200, // Time to wait for gallery to load so pointer events can be applied.
    validSwipeTime: 300, // How much time until swiping down is no longer a valid option
    // User configuration:
    minSwipeDistance: 120,// Minimum swipe distance required (px)
  };
  Edexal.addCSS(`
@media (width <= ${MAX_SCREEN_WIDTH}px) {
  /*Fixes 'Your account' navigation menu*/ 
  #js-SideNavOcm .uix_sidebar--scroller {
    margin-top: 149px;
    & .block-header {
        padding: 14px;
        margin: 0;
        font-size: 1.5rem;
        font-weight: 700;
        background-color: #37383a;
        color:#babbbc;
        &::before {
          content: "\\f007";
        }
    }
    & .block-body:nth-of-type(2) {
      margin-bottom: 20px;
    }
  }
  /*Fixes scroll buttons appearing behind threads*/
  .uix_fabBar {
    z-index: 50;
    & .u-scrollButtons {
      position: inherit;
      gap:10px;
      bottom: 60px;
      left: 45vw;
      &.is-active {
        opacity: 0.6;
      }
      & a:last-child {
        margin-left: 0;
      }
      & a.button.button--scroll {
        padding: 6px 12px;
      }
    }
  }
  .has-hiddenscroll .u-scrollButtons {
	  right: 35vw;
  }
  
  /*Fixes sidebar list of all online members & staff*/
  body .p-body-sidebar {
    margin-top: 64px;
    margin-bottom: 45px;
    &::after {
      margin:initial;
    }
  }
  /*Styles for reactions*/
  button.reaction.actionBar-action {
    position: relative;
    cursor: pointer;
  }
  .reactTooltip {
    position: absolute;
    bottom: 30px;
    right: -70px;
    width: 180px;
    z-index: 5;
    background-color: #242629;
    border: 1px solid #343638;
    border-radius: 8px;
    max-width: unset;
    gap: 2px;
  }
   .actionBar .reaction.reaction--imageHidden.reaction--1 i {
      padding-right: 0;
      margin-right: 0;
  } 
  .reaction.reaction--imageHidden img {
    display: unset;
  }
  .reaction.reaction--imageHidden > img {
    display: none;
  }
  .reaction-text {
    margin-left: 6px;
  }
  .has-reaction .reaction-text {
    margin-left: 0;
  }
  /*Fixes: Some game threads sticky header bar is messed up. Ex: Jujutsu Trainer*/
  div.uix_headerContainer div.p-navSticky {
      width: 100vw !important;
      &.is-sticky {
          right: auto !important;
      }
  }
  body {
    padding-top: inherit !important;
  }
  /*Fixes: Broken sticky bottom navbar on some game threads. Ex: JuJutsu Trainer*/
  div.uix_tabList {
    width: 100vw;
  }
  /*latest update header*/
  .headList {
    margin-top: 5px;
    font-size: 1.6rem !important;
  }
  /*Filter navbar in latest update*/
  #latest-page_sub-nav {
    position: sticky;
    z-index: 220;
    top: 60px;
  }
}`);

  function createCustomScrollBtns(topTargets, bottomTargets) {
    const uixFabBar = Edexal.newEl({element: 'div', class: ['uix_fabBar', 'uix_fabBar--active']});

    const divScrollButtons = Edexal.newEl({
      element: 'div',
      class: ['u-scrollButtons', 'js-scrollButtons'],
      "data-trigger-type": "both"
    });

    const aTopButton = Edexal.newEl({element: 'a', href: "#top", class: ['button--scroll', 'ripple-JsOnly', 'button']});
    const spanTopText = Edexal.newEl({element: 'span', class: ['button-text']});
    const iTop = Edexal.newEl({element: 'i', class: ['fa--xf', 'far', 'fa-arrow-up'], "aria-hidden": "true"});
    const spanTopSR = Edexal.newEl({element: 'span', class: ['u-srOnly'], text: 'Top'});

    const aBottomButton = Edexal.newEl({
      element: 'a',
      href: '#footer',
      class: ['button--scroll', 'ripple-JsOnly', 'button']
    });
    const spanBottomText = Edexal.newEl({element: 'span', class: ['button-text']});
    const iBottom = Edexal.newEl({element: 'i', class: ['fa--xf', 'far', 'fa-arrow-down'], "aria-hidden": "true"});
    const spanBottomSR = Edexal.newEl({element: 'span', class: ['u-srOnly'], text: 'Bottom'});

    Edexal.on(aTopButton, 'click', (e) => {
      e.preventDefault();
      const target = Edexal.$(topTargets[0]) ?? Edexal.$(topTargets[1]);
      target.scrollIntoView({
        block: 'start'
      });
    });
    Edexal.on(aBottomButton, 'click', (e) => {
      e.preventDefault();
      const target = Edexal.$(bottomTargets[0]) ?? Edexal.$(bottomTargets[1]);
      target.scrollIntoView({
        block: 'end'
      });
    });
    spanTopText.append(iTop, spanTopSR);
    aTopButton.append(spanTopText);
    divScrollButtons.append(aTopButton);

    spanBottomText.append(iBottom, spanBottomSR);
    aBottomButton.append(spanBottomText);
    divScrollButtons.append(aBottomButton);

    uixFabBar.append(divScrollButtons);
    return uixFabBar;
  }

  function setScrollBtn(selector, targetSelector, scrollType, altTargetSelector) {
    const scrollBtn = Edexal.$(selector);
    if (!scrollBtn) return false;
    scrollBtn.removeAttribute('data-xf-click');
    Edexal.on(scrollBtn, 'click', (e) => {
      e.preventDefault();
      const target = Edexal.$(targetSelector) ?? Edexal.$(altTargetSelector);
      target.scrollIntoView({
        block: scrollType
      });
    });
    return true;
  }

  function initScrollBtns() {
    const bottomTargets = ['div.block-outer:nth-child(4) > div:nth-child(1) > nav:nth-child(1) > div:nth-child(2)', '#footer.p-footer'];
    const topTargets = ['.block--messages', '#top'];
    const hasNewBtn = setScrollBtn('.uix_fabBar .u-scrollButtons a:last-child',
      bottomTargets[0],
      'end',
      bottomTargets[1]);
    if (hasNewBtn) {
      setScrollBtn('.uix_fabBar .u-scrollButtons a:first-child',
        topTargets[0],
        'start',
        topTargets[1]);
    } else {
      const footer = Edexal.$("#footer.p-footer");
      const scrollBtnDiv = createCustomScrollBtns(topTargets, bottomTargets);
      footer.insertAdjacentElement('afterend', scrollBtnDiv);
    }
  }

  function assignTabItem(name, pathURL, newFaIcons, itemPos) {
    const tabItem = Edexal.$(`.uix_tabBar .uix_tabList .uix_tabItem:nth-of-type(${itemPos})`);
    if (!tabItem) return;
    tabItem.href = pathURL;
    const icon = tabItem.querySelector('i');
    const classValues = icon.classList.values();
    for (const classVal of classValues) {
      if (classVal.startsWith('far') || classVal.startsWith('fa-')) {
        icon.classList.remove(classVal);
      }
    }
    icon.classList.add(...newFaIcons);
    const labelDiv = tabItem.querySelector('div');
    labelDiv.textContent = name;
  }

  function initTabItems() {
    assignTabItem('Latest Updates', '/sam/latest_alpha/', ['far', 'fa-gem'], 1);
    assignTabItem('Bookmarks', '/account/bookmarks/', ['far', 'fa-bookmark'], 2);
  }

  /*Removes all effects from tiles on Latest Update Page
   by removing all event listeners from tiles*/
  function removeTileHoverEffects() {
    Edexal.runOnLatestUpdatePage(() => {
      const tilesWrapper = Edexal.$(SELECTOR.latestUpdate);
      const tilesWrapperClone = tilesWrapper.cloneNode();
      tilesWrapperClone.append(...tilesWrapper.childNodes);
      const fragment = document.createDocumentFragment();
      fragment.append(tilesWrapperClone);
      tilesWrapper.replaceWith(fragment);
    });
  }

  function createReaction(idNum, altName) {
    const a = Edexal.newEl({
      element: 'a',
      href: `/posts/10864040/react?reaction_id=${idNum}`,
      class: ['reaction', `reaction--${idNum}`],
      'data-reaction-id': idNum
    });
    const img = Edexal.newEl({
      element: 'img',
      src: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',
      class: ['reaction-sprite', 'js-reaction'],
      alt: altName,
      title: altName,
      'data-extra-class': "tooltip--basic tooltip--noninteractive",
      'data-delay-in': 50,
      'data-delay-out': 50
    });
    a.append(img);
    return a;
  }

  function createReactionBar() {
    const divBar = Edexal.newEl({element: 'div', class: ['reactTooltip']});
    let reactions = [
      createReaction(1, 'Like'),
      createReaction(14, 'Heart'),
      createReaction(13, "Jizzed my pants"),
      createReaction(12, "Yay, update!"),
      createReaction(3, "Haha"),
      createReaction(9, "Hey there"),
      createReaction(4, "Wow"),
      createReaction(7, "Thinking Face"),
      createReaction(5, "Sad"),
      createReaction(18, "Disagree"),
      createReaction(8, "Angry")
    ];
    divBar.append(...reactions);
    REACTION_BAR = divBar;
  }

  function initReactionBtns(likeBtns) {
    for (const likeBtn of likeBtns) {
      let reactBtn;
      if (!likeBtn.classList.contains('has-reaction')) {
        reactBtn = Edexal.newEl({
          element: 'button',
          'data-post-id': likeBtn.getAttribute('data-th-react-plus-content-id')
        });
        likeBtn.classList.remove('reaction--small');
      } else {
        reactBtn = Edexal.newEl({element: 'a', href: likeBtn.href});
      }
      reactBtn.classList.add(...likeBtn.classList);
      reactBtn.append(...likeBtn.childNodes);
      likeBtn.replaceWith(reactBtn);
    }
  }

  function updateReactionBtnURLs(postId) {
    REACTION_BAR.querySelectorAll('a').forEach(el => {
      el.href = el.href.replace(/\/[0-9]+\//, `/${postId}/`);
    });
  }

  function addReactionBarEvent(e) {
    updateReactionBtnURLs(e.currentTarget.dataset.postId);
    e.currentTarget.append(REACTION_BAR);
  }

  function removeReactionBarEvent(e) {
    if (!e.target.closest(SELECTOR.reactionBtns)) {
      REACTION_BAR.remove();
    }
  }

  function addReactionBtnEvents() {
    const reactionBtns = Edexal.$$(SELECTOR.reactionBtns);
    for (const reactBtn of reactionBtns) {
      Edexal.on(reactBtn, 'click', addReactionBarEvent);
    }
  }

  function setRemoval() {
    const thread = Edexal.$(SELECTOR.thread);
    Edexal.on(thread, 'click', removeReactionBarEvent);
  }

  function initReaction() {
    const likeBtns = Edexal.$$(SELECTOR.likeBtns);
    if (!likeBtns || !likeBtns.length) return;
    createReactionBar();
    initReactionBtns(likeBtns);
    addReactionBtnEvents();
    setRemoval();
  }

  function removeBookmarkFilter() {
    setTimeout(() => {
      const filterMenuActive = Edexal.$("div.menu[id].is-active");
      if (!filterMenuActive) return;
      const filterBtnClose = Edexal.$(".select2-selection__choice__remove");
      if (!filterBtnClose) return;
      filterBtnClose.click();
    }, 200);
  }

  function initBookmarkLabel() {
    Edexal.runOnBookmarkPage(() => {
      const filtersLink = Edexal.$(".filterBar-menuTrigger");
      Edexal.on(filtersLink, "click", removeBookmarkFilter);
    });
  }

  function removeAdBanner() {
    const banner = Edexal.$('#tn7k9m2x');
    if (banner) {
      banner.remove();
    }
  }

  function initImageGallery() {
    const galleryImages = Edexal.$$('.message-threadStarterPost .js-lbImage');
    if (!galleryImages || !galleryImages.length) {
      return;
    }
    galleryImages.forEach(node => {
      Edexal.on(node, "click", () => {
        setTimeout(() => {
          const gallery = Edexal.$("div.lg");
          // In case 'close' button is pressed instead
          const closeBtn = gallery.querySelector(".lg-close");
          Edexal.on(closeBtn, "click", removeSwipeDownEvents);
          Edexal.on(gallery, "pointerdown", swipeStartEvent);
          Edexal.on(gallery, "pointerup", swipeEndEvent);
        }, SWIPE_DOWN_CONFIG.galleryEventDelay);
      });
    });
  }


  function swipeStartEvent(e) {
    if (e.pointerType !== "touch" || isZoomState()) {
      return;
    }
    SWIPE_DOWN_CONFIG.isValidSwipeDuration = true;
    SWIPE_DOWN_CONFIG.lastDownYPos = e.clientY;
    setTimeout(() => {
      resetSwipeDownStats();
    }, SWIPE_DOWN_CONFIG.validSwipeTime);
  }

  function swipeEndEvent(e) {
    if (e.pointerType !== "touch" || !SWIPE_DOWN_CONFIG.isValidSwipeDuration || isZoomState()) {
      resetSwipeDownStats();
      return;
    }
    if (SWIPE_DOWN_CONFIG.lastDownYPos + SWIPE_DOWN_CONFIG.minSwipeDistance < e.clientY) {
      resetSwipeDownStats();
      const closeBtn = Edexal.$(".lg-close");
      Edexal.off(closeBtn, "click", removeSwipeDownEvents);
      removeSwipeDownEvents();
      closeBtn.click();
    }
  }

  function isZoomState() {
    return Edexal.$('.lg-outer').classList.contains("lg-zoomed");
  }

  function removeSwipeDownEvents(e) {
    const imageGallery = Edexal.$("div.lg");
    Edexal.off(imageGallery, "pointerdown", swipeStartEvent);
    Edexal.off(imageGallery, "pointerup", swipeEndEvent);
    // If 'close' button was pressed instead.
    if (e) {
      Edexal.off(e.target, "click", removeSwipeDownEvents);
    }
  }

  function resetSwipeDownStats() {
    SWIPE_DOWN_CONFIG.isValidSwipeDuration = false;
    SWIPE_DOWN_CONFIG.lastDownYPos = 0;
  }

  function stickyNavBar() {
    Edexal.runOnLatestUpdatePage(() => {
      modifyHeaderLinks();
      preventScrollAfterClose();
      scrollToOptionsPanel();
    });
  }

  function modifyHeaderLinks() {
    const headerNav = Edexal.$("ul.p-nav-list");
    for (let i = 0; i < 2; i++) {
      headerNav.children.item(0).remove();// removes 'DOWNLOADS' & 'FORUMS'
    }
    headerNav.children[0].firstElementChild.classList.add("headList");// style 'SEARCH'
  }

  function preventScrollAfterClose() {
    const closeBtn = Edexal.$("#filter-close");
    Edexal.on(closeBtn, "click", (e) => {
      e.preventDefault();
      e.stopImmediatePropagation();
      const filterDrawer = Edexal.$(SELECTOR.latestFilterDrawer);
      filterDrawer.classList.add("filter-hidden");
      setTimeout(() => filterDrawer.style.display = "none", 250);
      document.body.style.overflow = "hidden scroll";
    });
  }

  function scrollToOptionsPanel() {
    const optionsBtn = Edexal.$("#controls_toggle-options-panel");
    Edexal.on(optionsBtn, 'click', () => {
      window.scroll(0, 200);
    });
  }

  function run() {
    //Run only on mobile
    if (Edexal.$("html").clientWidth > MAX_SCREEN_WIDTH) return;
    initTabItems();
    initScrollBtns();
    initReaction();
    initBookmarkLabel();
    setTimeout(removeTileHoverEffects, 2000);
    removeAdBanner();
    initImageGallery();
    stickyNavBar();
  }

  run();
})()