Smartschool_Edit

Smartschool aanpassen met extra functies: voorspelling van punten, nav kleuren, etc.

// ==UserScript==
// @name         Smartschool_Edit
// @namespace    http://tampermonkey.net/
// @version      0.8.1
// @description  Smartschool aanpassen met extra functies: voorspelling van punten, nav kleuren, etc.
// @license      MIT
// @author       andreasthuis
// @match        https://*.smartschool.be/*
// @exclude      https://*.smartschool.be/index.php?module=Messages&file=composeMessage&*
// @exclude      https://*.smartschool.be/Upload/*
// @exclude      https://wopi2.smartschool.be/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=smartschool.be
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @connect      raw.githubusercontent.com
// @connect      proud-art-8cdd.andreasdeborger27.workers.dev
// @run-at       document-start
// ==/UserScript==

(function() {
  'use strict';

  const baseUrl = "https://raw.githubusercontent.com/andreasthuis/Smartschool_edit/main/";
  const cssFiles = ["root.css"];
  const jsFiles = ["root.js"];

  // --- Loaders ---
  function loadCSS(path) {
    GM_xmlhttpRequest({
      method: "GET",
      url: baseUrl + path,
      onload: res => {
        if (res.status === 200) GM_addStyle(res.responseText);
        else console.error("❌ Failed to load CSS:", res.status, path);
      },
      onerror: err => console.error("❌ CSS request failed:", err)
    });
  }

  function loadJS(path) {
    GM_xmlhttpRequest({
      method: "GET",
      url: baseUrl + path,
      onload: res => {
        if (res.status === 200) {
          const script = document.createElement("script");
          script.textContent = res.responseText;
          document.documentElement.appendChild(script);
          script.remove();
        } else {
          console.error("❌ Failed to load JS:", res.status, path);
        }
      },
      onerror: err => console.error("❌ JS request failed:", err)
    });
  }

  const global = (typeof unsafeWindow !== "undefined") ? unsafeWindow : window;
  global.smartschool_loadScript = loadJS;
  global.smartschool_loadStyles = loadCSS;

  global.smartschool_webRequest = function(method, url, data = null, headers = {}) {
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method,
        url,
        headers: Object.assign({ "Content-Type": "application/json" }, headers),
        data: data ? JSON.stringify(data) : undefined,
        onload: res => {
          try { resolve(JSON.parse(res.responseText)); }
          catch { resolve(res.responseText); }
        },
        onerror: err => reject(err),
        ontimeout: () => reject(new Error("Request timed out"))
      });
    });
  };

  global.smartschoolSettings = {
    get: (key, def) => {
      try { return GM_getValue(key, def); }
      catch (e) { console.error("[smartschoolSettings] get error", e); return def; }
    },
    set: (key, value) => {
      try { GM_setValue(key, value); }
      catch (e) { console.error("[smartschoolSettings] set error", e); }
    }
  };

  const $ = window.jQuery;

  /**
   * Wait until a selector appears in the DOM
   * @param {string} selector
   * @param {number} timeout - in ms (default 10s)
   */
  function waitForSelector(selector, timeout = 10000) {
    return new Promise((resolve, reject) => {
      const el = $(selector);
      if (el.length) return resolve(el);

      const observer = new MutationObserver(() => {
        const e = $(selector);
        if (e.length) {
          observer.disconnect();
          resolve(e);
        }
      });

      observer.observe(document.documentElement, { childList: true, subtree: true });

      if (timeout > 0) {
        setTimeout(() => {
          observer.disconnect();
          reject(new Error(`Timeout waiting for selector: ${selector}`));
        }, timeout);
      }
    });
  }

  /**
   * Adds a persistent element (auto-reappears if removed)
   * @param {object} options
   * @param {string} options.parentSelector - Where to insert
   * @param {string} options.elementId - Unique ID
   * @param {string|jQuery|HTMLElement} options.content - Inner HTML / element to insert
   * @param {string} [options.insertType='append'] - append | prepend | before | after
   * @param {function} [options.onClick] - Optional click handler
   */
  async function addPersistentElement({
    parentSelector,
    elementId,
    content,
    insertType = 'append',
    onClick
  }) {
    const parent = await waitForSelector(parentSelector, 15000);
    const parentEl = parent[0];
    if (!parentEl) return console.error('Parent not found:', parentSelector);

    if (!$(`#${elementId}`).length) {
      const $el = $('<div/>').attr('id', elementId).addClass('auto-element');

      if (typeof content === 'string') $el.html(content);
      else $el.append(content);

      if (onClick) $el.on('click', onClick);

      switch (insertType) {
        case 'prepend': parent.prepend($el); break;
        case 'before': parent.before($el); break;
        case 'after': parent.after($el); break;
        default: parent.append($el);
      }
    }

    const observer = new MutationObserver((mutations) => {
      for (const mutation of mutations) {
        if (mutation.type === 'childList' && mutation.removedNodes.length) {
          mutation.removedNodes.forEach(node => {
            if (node.id === elementId) {
              console.warn(`[PersistentElement] Re-adding ${elementId}`);
              addPersistentElement({ parentSelector, elementId, content, insertType, onClick });
            }
          });
        }
      }
    });
    observer.observe(parentEl, { childList: true, subtree: false });
  }

  global.smartschool_addElement = addPersistentElement;

  function loadAllAssets() {
    cssFiles.forEach(loadCSS);
    jsFiles.forEach(loadJS);
  }

  loadAllAssets();

})();