Danbooru Theme Toggle (SunCalc + IP Geolocation)

Automatically toggles native dark mode based on local sunset/sunrise times

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name         Danbooru Theme Toggle (SunCalc + IP Geolocation)
// @namespace    Danbooru
// @version      1.7
// @description  Automatically toggles native dark mode based on local sunset/sunrise times
// @author       Dramorian
// @match        https://danbooru.donmai.us/*
// @match        https://aibooru.online/*
// @run-at       document-end
// @require      https://cdn.jsdelivr.net/npm/[email protected]/suncalc.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/global.min.js
// @license      MIT
// ==/UserScript==

/**
 * Manages automatic theme toggling for Danbooru based on sunrise/sunset times.
 * @class
 */
class DanbooruThemeToggle {
  #config = {
    debug: true,
    cacheKey: 'danbooru_theme_location',
    cacheDuration: 12 * 60 * 60 * 1000, // 12 hours in milliseconds
  };

  /**
   * Logs messages if debug mode is enabled.
   * @param {...any} args - Values to log.
   */
  #log(...args) {
    if (this.#config.debug) console.log('[DanbooruThemeToggle]', ...args);
  }

  /**
   * Logs errors if debug mode is enabled.
   * @param {...any} args - Error details to log.
   */
  #logError(...args) {
    if (this.#config.debug) console.error('[DanbooruThemeToggle]', ...args);
  }

  /**
   * Retrieves or fetches location data, with caching.
   * @returns {Promise<{latitude: number, longitude: number, timezone: string}>}
   */
  async #getLocationData() {
    this.#log('Checking for cached location data...');
    const cachedData = localStorage.getItem(this.#config.cacheKey);

    if (cachedData) {
      const { timestamp, location } = JSON.parse(cachedData);
      const cacheAge = Temporal.Now.instant().since(
        Temporal.Instant.fromEpochMilliseconds(timestamp)
      ).total({ unit: 'millisecond' });

      if (cacheAge < this.#config.cacheDuration) {
        this.#log('Using cached location:', location);
        return location;
      }
      this.#log('Cache expired, fetching new location data');
    } else {
      this.#log('No cached data found, fetching new location data');
    }

    try {
      this.#log('Fetching location from ipapi.co...');
      const response = await fetch('https://ipapi.co/json/', {
        signal: AbortSignal.timeout(5000), // ES15 AbortSignal timeout
      });
      const location = await response.json();

      this.#log('Received location data:', {
        lat: location.latitude,
        lng: location.longitude,
        timezone: location.timezone,
      });

      localStorage.setItem(
        this.#config.cacheKey,
        JSON.stringify({
          timestamp: Temporal.Now.instant().epochMilliseconds,
          location,
        })
      );

      return location;
    } catch (error) {
      this.#logError('Location fetch failed:', error);
      throw new Error('Failed to fetch location data', { cause: error });
    }
  }

  /**
   * Updates the theme based on sunrise/sunset times.
   * @returns {Promise<void>}
   */
  async #updateThemeBasedOnSun() {
    this.#log('Starting theme update...');

    if (typeof Danbooru === 'undefined' || !Danbooru?.CurrentUser) {
      this.#logError('Danbooru API unavailable');
      return;
    }

    try {
      const { latitude, longitude, timezone } = await this.#getLocationData();
      const lat = Number.parseFloat(latitude);
      const lng = Number.parseFloat(longitude);

      // Calculate sun times using Temporal API
      const now = Temporal.Now.zonedDateTimeISO(timezone);
      const { sunrise, sunset } = SunCalc.getTimes(
        new Date(now.toInstant().epochMilliseconds),
        lat,
        lng
      );

      this.#log('Sun times:', {
        sunrise: sunrise.toUTCString(),
        sunset: sunset.toUTCString(),
      });

      // Format times using Temporal
      const formatter = (date) =>
        Temporal.Instant.fromEpochMilliseconds(date.getTime())
          .toZonedDateTimeISO(timezone)
          .toLocaleString('en-US', {
            hour: '2-digit',
            minute: '2-digit',
            hourCycle: 'h24',
          });

      const currentTime = now.toLocaleString('en-US', {
        hour: '2-digit',
        minute: '2-digit',
        hourCycle: 'h24',
      });
      const sunriseTime = formatter(sunrise);
      const sunsetTime = formatter(sunset);

      this.#log('Formatted times:', { currentTime, sunriseTime, sunsetTime });

      // Determine theme
      const isNightTime =
        currentTime >= sunsetTime || currentTime < sunriseTime;
      const currentThemeIsDark = Danbooru.CurrentUser.darkMode();

      if (isNightTime !== currentThemeIsDark) {
        const newTheme = isNightTime ? 'dark' : 'light';
        this.#log(`Updating to ${newTheme} mode...`);
        await Danbooru.CurrentUser.update({ theme: newTheme });
        Danbooru.Utility.notice(`Theme updated to ${newTheme} mode.`);
        this.#log('Theme updated, reloading page');
        window.location.reload();
      } else {
        this.#log('Theme matches time of day, no update needed');
      }
    } catch (error) {
      this.#logError('Theme update failed:', error);
    }
  }

  /**
   * Initializes the theme toggle script.
   * @returns {Promise<void>}
   */
  async init() {
    this.#log('Script initialized');
    await this.#updateThemeBasedOnSun();
  }
}

// Start the script
new DanbooruThemeToggle().init().catch((error) =>
  console.error('[DanbooruThemeToggle] Initialization failed:', error)
);