Facebook Event → Download ICS

Adds a button on Facebook Event pages to download the event as an .ics file

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Facebook Event → Download ICS
// @namespace    https://djpanaflex.com/
// @version      1.0
// @description  Adds a button on Facebook Event pages to download the event as an .ics file
// @match        https://www.facebook.com/events/*
// @match        https://m.facebook.com/events/*
// @grant        none
// @license      none
// @run-at       document-idle
// ==/UserScript==

(function () {
  'use strict';

  // Extract the numeric event ID from the URL path (/events/123456789012345/)
  function getEventIdFromPath() {
    const parts = location.pathname.split('/').filter(Boolean);
    const idx = parts.indexOf('events');
    if (idx !== -1 && parts[idx + 1]) {
      const candidate = parts[idx + 1].replace(/\D+/g, ''); // keep digits only
      if (candidate.length >= 9) return candidate; // FB event IDs are long
    }
    return null;
  }

  function buildIcsUrl(eventId) {
    return `https://www.facebook.com/events/ical/export/?eid=${eventId}`;
  }

  // Create a small floating button (robust against FB DOM churn)
  function injectButton(icsUrl) {
    // Avoid duplicates
    if (document.getElementById('fb-ics-download-btn')) return;

    const btn = document.createElement('a');
    btn.id = 'fb-ics-download-btn';
    btn.href = icsUrl;
    btn.target = '_blank';
    btn.rel = 'noopener';
    btn.textContent = 'Download ICS';

    Object.assign(btn.style, {
      position: 'fixed',
      right: '16px',
      bottom: '16px',
      zIndex: 999999,
      padding: '10px 14px',
      borderRadius: '999px',
      background: '#1877f2',
      color: '#fff',
      fontSize: '14px',
      fontWeight: '600',
      textDecoration: 'none',
      boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
      fontFamily: 'system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Arial, sans-serif',
    });

    btn.onmouseenter = () => (btn.style.filter = 'brightness(1.1)');
    btn.onmouseleave = () => (btn.style.filter = 'none');

    document.body.appendChild(btn);
  }

  // Because FB is SPA-ish, observe URL changes and (re)inject when on an event
  let lastEventId = null;

  function tryInject() {
    const eid = getEventIdFromPath();
    if (!eid) return;
    if (eid === lastEventId && document.getElementById('fb-ics-download-btn')) return;
    lastEventId = eid;
    injectButton(buildIcsUrl(eid));
  }

  // Initial run
  tryInject();

  // Re-run on navigation changes
  const pushState = history.pushState;
  history.pushState = function () {
    const ret = pushState.apply(this, arguments);
    setTimeout(tryInject, 300);
    return ret;
  };
  window.addEventListener('popstate', () => setTimeout(tryInject, 300));

  // Also poll a bit, in case of dynamic loads
  let tries = 0;
  const iv = setInterval(() => {
    tryInject();
    if (++tries > 20) clearInterval(iv); // stop after a few seconds
  }, 500);
})();