Cookie Updater

udemy cookies + organize courses

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Cookie Updater
// @description  udemy cookies + organize courses
// @namespace    https://greasyfork.org/users/1508709
// @version      3.0.9
// @author       https://github.com/sitien173
// @match        *://*.udemy.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_cookie
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @connect      udemy-cookies-worker-commercial.sitienbmt.workers.dev
// @run-at       document-start
// @source       https://github.com/sitien173/tampermonkey
// ==/UserScript==
(function () {
  'use strict';
  const workerUrl = 'https://udemy-cookies-worker-commercial.sitienbmt.workers.dev';
  // =====================================================
  // CONFIGURATION
  // =====================================================
  const DEFAULT_CONFIG = {
    licenseKey: '',
    retryAttempts: 3,
    showUiButtons: true,
    showFolderOrganizer: true,
  };
  let config = { ...DEFAULT_CONFIG };
  let folders = [];
  let isOrganizerPopupOpen = false;
  let isSyncing = false;
  let lastSyncTime = 0;
  // =====================================================
  // STORAGE & INITIALIZATION
  // =====================================================
  function loadConfig() {
    const savedConfig = GM_getValue('config', {});
    config = { ...DEFAULT_CONFIG, ...savedConfig };
  }

  function saveConfig() {
    GM_setValue('config', config);
  }

  function getOrCreateDeviceId() {
    let id = GM_getValue('deviceId', '');
    if (!id) {
      try {
        if (crypto && crypto.randomUUID) {
          id = crypto.randomUUID();
        }
      } catch {
        // Ignore randomUUID errors and fall back to generated id
      }
      if (!id) {
        id = Date.now().toString(36) + Math.random().toString(36).slice(2, 10);
      }
      id = id.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 64);
      GM_setValue('deviceId', id);
    }
    return id;
  }

  // Get user info for display
  function getUserInfo() {
    const totalCourses = folders.reduce(
      (sum, f) => sum + (f.courses?.length || f.course_count || 0),
      0
    );
    return {
      licenseKey: config.licenseKey ? config.licenseKey.slice(0, 8) + '****' : 'Not set',
      deviceId: getOrCreateDeviceId().slice(0, 12) + '...',
      totalFolders: folders.length,
      totalCourses: totalCourses,
    };
  }

  // =====================================================
  // API HELPERS
  // =====================================================
  function apiRequest(method, endpoint, body = null) {
    return new Promise((resolve, reject) => {
      const url = workerUrl + endpoint;

      console.log('Making API request to:', url);
      GM_xmlhttpRequest({
        method: method,
        url: url,
        headers: {
          'Content-Type': 'application/json',
          'X-License-Key': config.licenseKey,
          'X-Device-Id': getOrCreateDeviceId(),
        },
        data: body ? JSON.stringify(body) : null,
        onload: function (response) {
          try {
            const data = JSON.parse(response.responseText);
            if (response.status >= 200 && response.status < 300) {
              resolve(data);
            } else {
              reject(new Error(data.error || `HTTP ${response.status}`));
            }
          } catch {
            reject(new Error('Invalid JSON response'));
          }
        },
        onerror: function (_error) {
          reject(new Error('Network error'));
        },
      });
    });
  }

  // =====================================================
  // FOLDER API OPERATIONS
  // =====================================================
  async function syncFoldersFromServer() {
    if (!config.licenseKey) {
      console.log('No license key configured, using local storage');
      loadFoldersFromLocal();
      return;
    }

    if (isSyncing) return;
    isSyncing = true;

    try {
      const data = await apiRequest('GET', '/api/sync');
      folders = data.folders || [];
      lastSyncTime = data.synced_at || Date.now();

      // Cache locally for offline use
      GM_setValue('cachedFolders', folders);
      GM_setValue('lastSyncTime', lastSyncTime);

      console.log(`Synced ${folders.length} folders from server`);
    } catch (error) {
      console.error('Failed to sync from server:', error);
      // Fall back to local cache
      loadFoldersFromLocal();
    } finally {
      isSyncing = false;
    }
  }

  function loadFoldersFromLocal() {
    const cached = GM_getValue('cachedFolders', null);
    if (cached && Array.isArray(cached)) {
      folders = cached;
    } else {
      // Default folders for first-time users without license
      folders = [
        { id: 1, name: 'My Courses', color: '#6366f1', courses: [], course_count: 0 },
        { id: 2, name: 'Favorites', color: '#ec4899', courses: [], course_count: 0 },
        { id: 3, name: 'In Progress', color: '#f59e0b', courses: [], course_count: 0 },
        { id: 4, name: 'Completed', color: '#10b981', courses: [], course_count: 0 },
      ];
    }
  }

  async function initDefaultFolders() {
    if (!config.licenseKey) return;

    try {
      await apiRequest('POST', '/api/init');
      await syncFoldersFromServer();
    } catch (error) {
      console.error('Failed to initialize default folders:', error);
    }
  }

  async function createFolderAPI(name, color) {
    if (!config.licenseKey) {
      // Local mode
      const newFolder = {
        id: Date.now(),
        name: name,
        color: color,
        courses: [],
        course_count: 0,
      };
      folders.push(newFolder);
      GM_setValue('cachedFolders', folders);
      return newFolder;
    }

    const data = await apiRequest('POST', '/api/folders', { name, color });
    await syncFoldersFromServer();
    return data.folder;
  }

  async function updateFolderAPI(folderId, updates) {
    if (!config.licenseKey) {
      // Local mode
      const folder = folders.find((f) => f.id === folderId);
      if (folder) {
        Object.assign(folder, updates);
        GM_setValue('cachedFolders', folders);
      }
      return folder;
    }

    const data = await apiRequest('PUT', `/api/folders/${folderId}`, updates);
    await syncFoldersFromServer();
    return data.folder;
  }

  async function deleteFolderAPI(folderId) {
    if (!config.licenseKey) {
      // Local mode
      folders = folders.filter((f) => f.id !== folderId);
      GM_setValue('cachedFolders', folders);
      return;
    }

    await apiRequest('DELETE', `/api/folders/${folderId}`);
    await syncFoldersFromServer();
  }

  async function addCourseToFoldersAPI(folderIds, courseInfo) {
    if (!config.licenseKey) {
      // Local mode
      let added = 0;
      folderIds.forEach((folderId) => {
        const folder = folders.find((f) => f.id === folderId);
        if (folder) {
          if (!folder.courses) folder.courses = [];
          const exists = folder.courses.some(
            (c) => c.udemy_course_id === courseInfo.id || c.id === courseInfo.id
          );
          if (!exists) {
            folder.courses.push({
              udemy_course_id: courseInfo.id,
              title: courseInfo.title,
              url: courseInfo.url,
              image_url: courseInfo.image,
              instructor: courseInfo.instructor,
              added_at: Math.floor(Date.now() / 1000),
            });
            folder.course_count = folder.courses.length;
            added++;
          }
        }
      });
      GM_setValue('cachedFolders', folders);
      return { added };
    }

    const data = await apiRequest('POST', '/api/courses/add-to-folders', {
      folder_ids: folderIds,
      course_id: courseInfo.id,
      title: courseInfo.title,
      url: courseInfo.url,
      image_url: courseInfo.image,
      instructor: courseInfo.instructor,
    });
    await syncFoldersFromServer();
    return data;
  }

  async function removeCourseFromFolderAPI(folderId, courseId) {
    console.log('removeCourseFromFolderAPI called:', {
      folderId,
      courseId,
      hasLicenseKey: !!config.licenseKey,
    });

    if (!config.licenseKey) {
      // Local mode
      const folder = folders.find((f) => f.id === folderId);
      if (folder && folder.courses) {
        const beforeCount = folder.courses.length;
        folder.courses = folder.courses.filter((c) => {
          const cId = c.course_id || c.id;
          return cId !== courseId && cId !== String(courseId);
        });
        folder.course_count = folder.courses.length;
        console.log('Local remove - before:', beforeCount, 'after:', folder.course_count);
      }
      GM_setValue('cachedFolders', folders);
      return;
    }

    try {
      console.log('Calling API DELETE:', `/api/folders/${folderId}/courses/${courseId}`);
      const result = await apiRequest('DELETE', `/api/folders/${folderId}/courses/${courseId}`);
      console.log('API DELETE result:', result);
      await syncFoldersFromServer();
    } catch (error) {
      console.error('API DELETE error:', error);
      throw error;
    }
  }

  // =====================================================
  // LESSON PROGRESS TRACKING
  // =====================================================
  let lastSavedLessonUrl = '';
  let lessonSaveTimeout = null;

  function isLessonPage() {
    // Lesson URLs look like: /course/{course-slug}/learn/lecture/{lecture-id}
    return /\/course\/[^/]+\/learn\//.test(window.location.pathname);
  }

  function getCourseSlugFromUrl(url = window.location.href) {
    const match = url.match(/\/course\/([^/?]+)/);
    return match ? match[1] : null;
  }

  function isCourseInFolders(courseSlug) {
    for (const folder of folders) {
      if (folder.courses) {
        for (const course of folder.courses) {
          const cSlug = course.udemy_course_id || course.course_id;
          if (cSlug === courseSlug) {
            return true;
          }
        }
      }
    }
    return false;
  }

  async function saveLessonProgress(lessonUrl) {
    const courseSlug = getCourseSlugFromUrl(lessonUrl);
    if (!courseSlug) return;

    // Only save if course is in our folders
    if (!isCourseInFolders(courseSlug)) {
      console.log('Course not in folders, skipping lesson save:', courseSlug);
      return;
    }

    // Don't save the same URL twice
    if (lessonUrl === lastSavedLessonUrl) return;

    if (!config.licenseKey) {
      // Local mode - save to local storage
      const lessonProgress = GM_getValue('lessonProgress', {});
      lessonProgress[courseSlug] = lessonUrl;
      GM_setValue('lessonProgress', lessonProgress);
      lastSavedLessonUrl = lessonUrl;
      console.log('Saved lesson progress locally:', courseSlug, lessonUrl);
      
      // Update local folders cache
      for (const folder of folders) {
        if (folder.courses) {
          for (const course of folder.courses) {
            const cSlug = course.udemy_course_id || course.course_id;
            if (cSlug === courseSlug) {
              course.last_lesson_url = lessonUrl;
            }
          }
        }
      }
      GM_setValue('cachedFolders', folders);
      return;
    }

    try {
      await apiRequest('PUT', '/api/courses/progress', {
        course_id: courseSlug,
        last_lesson_url: lessonUrl,
      });
      lastSavedLessonUrl = lessonUrl;
      console.log('Saved lesson progress to server:', courseSlug, lessonUrl);
      
      // Update local cache
      for (const folder of folders) {
        if (folder.courses) {
          for (const course of folder.courses) {
            const cSlug = course.udemy_course_id || course.course_id;
            if (cSlug === courseSlug) {
              course.last_lesson_url = lessonUrl;
            }
          }
        }
      }
    } catch (error) {
      console.error('Failed to save lesson progress:', error);
    }
  }

  function debouncedSaveLessonProgress(url) {
    if (lessonSaveTimeout) {
      clearTimeout(lessonSaveTimeout);
    }
    // Debounce to avoid saving too frequently during rapid navigation
    lessonSaveTimeout = setTimeout(() => {
      saveLessonProgress(url);
    }, 2000); // Wait 2 seconds before saving
  }

  function startLessonTracking() {
    let lastUrl = window.location.href;

    // Initial check
    if (isLessonPage()) {
      debouncedSaveLessonProgress(lastUrl);
    }

    // Watch for URL changes (SPA navigation)
    const observer = new MutationObserver(() => {
      if (window.location.href !== lastUrl) {
        lastUrl = window.location.href;
        if (isLessonPage()) {
          debouncedSaveLessonProgress(lastUrl);
        }
      }
    });

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

    // Also listen to popstate for back/forward navigation
    window.addEventListener('popstate', () => {
      if (isLessonPage()) {
        debouncedSaveLessonProgress(window.location.href);
      }
    });

    // Check periodically as backup
    setInterval(() => {
      if (window.location.href !== lastUrl) {
        lastUrl = window.location.href;
        if (isLessonPage()) {
          debouncedSaveLessonProgress(lastUrl);
        }
      }
    }, 3000);
  }

  function getCourseOpenUrl(course) {
    // Return last lesson URL if available, otherwise the course landing page
    if (course.last_lesson_url) {
      return course.last_lesson_url;
    }
    // For local mode, check local storage
    if (!config.licenseKey) {
      const lessonProgress = GM_getValue('lessonProgress', {});
      const courseSlug = course.udemy_course_id || course.course_id;
      if (lessonProgress[courseSlug]) {
        return lessonProgress[courseSlug];
      }
    }
    return course.url || '#';
  }

  async function loadCoursesForFolder(folderId) {
    if (!config.licenseKey) {
      const folder = folders.find((f) => f.id === folderId);
      return folder?.courses || [];
    }

    try {
      const data = await apiRequest('GET', `/api/folders/${folderId}/courses`);
      // Update local cache
      const folder = folders.find((f) => f.id === folderId);
      if (folder) {
        folder.courses = data.courses || [];
      }
      return data.courses || [];
    } catch (error) {
      console.error('Failed to load courses:', error);
      const folder = folders.find((f) => f.id === folderId);
      return folder?.courses || [];
    }
  }

  // =====================================================
  // COOKIE MANAGEMENT
  // =====================================================
  async function fetchCookiesFromWorker() {
    let lastError;

    for (let attempt = 1; attempt <= config.retryAttempts; attempt++) {
      try {
        console.log(`Fetching cookies from worker (attempt ${attempt}/${config.retryAttempts})...`);

        return new Promise((resolve, reject) => {
          GM_xmlhttpRequest({
            method: 'GET',
            url:
              workerUrl +
              '?key=' +
              encodeURIComponent(config.licenseKey) +
              '&device=' +
              encodeURIComponent(getOrCreateDeviceId()),
            onload: function (response) {
              if (response.status === 200) {
                try {
                  if (response.responseText === '{}') {
                    reject(new Error('Invalid license key'));
                    return;
                  }
                  const data = JSON.parse(response.responseText);
                  if (data.error) {
                    reject(new Error(data.error));
                    return;
                  }
                  if (Array.isArray(data)) {
                    console.log(`Successfully fetched ${data.length} cookies from worker`);
                    resolve(data);
                  } else {
                    reject(new Error('Invalid response format'));
                  }
                } catch (error) {
                  console.error('Failed to parse JSON response:', error);
                  reject(error);
                }
              } else {
                reject(new Error(`HTTP ${response.status}: ${response.statusText}`));
              }
            },
            onerror: function (error) {
              console.error('Network error:', error);
              reject(error);
            },
          });
        });
      } catch (error) {
        lastError = error;
        console.error(`Attempt ${attempt} failed:`, error);

        if (attempt < config.retryAttempts) {
          await new Promise((resolve) => setTimeout(resolve, 2000));
        }
      }
    }

    throw new Error(
      `Failed to fetch cookies after ${config.retryAttempts} attempts. Last error: ${lastError.message}`
    );
  }

  function prepareCookie(cookie, url) {
    const newCookie = {
      name: cookie.name || '',
      value: cookie.value || '',
      url: url,
      path: cookie.path || '/',
      secure: cookie.secure || false,
      httpOnly: cookie.httpOnly || false,
      expirationDate: cookie.expirationDate || null,
    };

    if (cookie.hostOnly) {
      newCookie.domain = null;
    } else if (cookie.domain) {
      newCookie.domain = cookie.domain;
    }

    let sameSite = cookie.sameSite;
    if (sameSite) {
      const sameSiteLower = sameSite.toLowerCase();
      if (sameSiteLower === 'no_restriction' || sameSiteLower === 'none') {
        sameSite = 'none';
        newCookie.secure = true;
      } else if (sameSiteLower === 'lax') {
        sameSite = 'lax';
      } else if (sameSiteLower === 'strict') {
        sameSite = 'strict';
      } else {
        sameSite = 'no_restriction';
      }
    } else {
      sameSite = 'no_restriction';
    }

    newCookie.sameSite = sameSite;

    if (cookie.session) {
      newCookie.expirationDate = null;
    }

    return newCookie;
  }

  function saveCookie(cookie, url) {
    const preparedCookie = prepareCookie(cookie, url);
    const gmAvailable =
      typeof GM_cookie !== 'undefined' && GM_cookie && typeof GM_cookie.set === 'function';

    if (gmAvailable) {
      return new Promise((resolve, reject) => {
        GM_cookie.set(preparedCookie, (result, error) => {
          if (error) {
            console.error('Failed to save cookie:', error);
            reject(error);
          } else {
            console.log(`Successfully saved cookie: ${cookie.name}`);
            resolve(result);
          }
        });
      });
    }

    return new Promise((resolve) => {
      let cookieStr = `${preparedCookie.name}=${encodeURIComponent(preparedCookie.value)}`;
      cookieStr += `; path=${preparedCookie.path}`;

      if (preparedCookie.domain && !cookie.hostOnly) {
        cookieStr += `; domain=${preparedCookie.domain}`;
      }

      if (preparedCookie.secure) {
        cookieStr += '; Secure';
      }

      if (preparedCookie.sameSite) {
        const s = preparedCookie.sameSite.toLowerCase();
        if (s === 'lax' || s === 'strict' || s === 'none') {
          cookieStr += `; SameSite=${s.charAt(0).toUpperCase() + s.slice(1)}`;
          if (s === 'none') {
            cookieStr += '; Secure';
          }
        }
      }

      if (preparedCookie.expirationDate) {
        const d = new Date(0);
        d.setUTCSeconds(preparedCookie.expirationDate);
        cookieStr += `; Expires=${d.toUTCString()}`;
      }

      document.cookie = cookieStr;
      console.warn('GM_cookie not available, used document.cookie fallback.');
      resolve(true);
    });
  }

  async function removeCookie(name, url, cookie) {
    const gmAvailable =
      typeof GM_cookie !== 'undefined' && GM_cookie && typeof GM_cookie.delete === 'function';

    if (gmAvailable) {
      if (cookie && cookie.domain) {
        const domains = [
          cookie.domain,
          '.' + cookie.domain.replace(/^\./, ''),
          cookie.domain.replace(/^\./, ''),
        ];

        for (const domain of domains) {
          try {
            await new Promise((resolve) => {
              GM_cookie.delete(
                {
                  name: name,
                  url: url,
                  domain: domain,
                },
                (result, error) => {
                  if (!error) {
                    console.log(`Successfully removed cookie: ${name} for domain: ${domain}`);
                  }
                  resolve(!error);
                }
              );
            });
          } catch {
            // Continue attempting deletion for other domains
          }
        }
      }

      return new Promise((resolve) => {
        GM_cookie.delete(
          {
            name: name,
            url: url,
          },
          (result, error) => {
            if (!error) {
              console.log(`Successfully removed cookie: ${name}`);
            }
            resolve(!error);
          }
        );
      });
    }

    return new Promise((resolve) => {
      const paths = ['/', cookie?.path || '/'];
      paths.forEach((path) => {
        document.cookie = `${name}=; path=${path}; Expires=Thu, 01 Jan 1970 00:00:00 GMT`;
        if (cookie?.domain) {
          document.cookie = `${name}=; domain=${cookie.domain}; path=${path}; Expires=Thu, 01 Jan 1970 00:00:00 GMT`;
        }
      });
      resolve(true);
    });
  }

  function getAllCookies(url) {
    const gmAvailable =
      typeof GM_cookie !== 'undefined' && GM_cookie && typeof GM_cookie.list === 'function';
    if (gmAvailable) {
      return new Promise((resolve, reject) => {
        GM_cookie.list({ url: url }, (cookies, error) => {
          if (error) {
            console.error('Failed to get cookies:', error);
            reject(error);
          } else {
            resolve(cookies);
          }
        });
      });
    }
    return new Promise((resolve) => {
      const cookieStr = document.cookie || '';
      const pairs = cookieStr ? cookieStr.split('; ') : [];
      const results = pairs.map((p) => {
        const eqIdx = p.indexOf('=');
        const name = eqIdx >= 0 ? p.slice(0, eqIdx) : p;
        const value = eqIdx >= 0 ? decodeURIComponent(p.slice(eqIdx + 1)) : '';
        return { name, value };
      });
      resolve(results);
    });
  }

  async function updateCookiesFromWorker(silentMode = false) {
    if (!silentMode) {
      showNotification('Starting cookie update...', 'info');
    }
    try {
      const newCookies = await fetchCookiesFromWorker();

      if (!newCookies || !Array.isArray(newCookies) || newCookies.length === 0) {
        console.log('No cookies were fetched from worker.');
        if (!silentMode) {
          showNotification('No cookies were fetched from worker.', 'warning');
        }
        return { success: false, message: 'No cookies fetched' };
      }

      const currentUrl = window.location.href;
      const existingCookies = await getAllCookies(currentUrl);

      let removedCount = 0;
      let successCount = 0;
      let errorCount = 0;

      console.log(`Removing all ${existingCookies.length} existing cookies...`);
      if (!silentMode) {
        showNotification(`Removing ${existingCookies.length} existing cookies...`, 'info');
      }

      for (const existingCookie of existingCookies) {
        try {
          await removeCookie(existingCookie.name, currentUrl, existingCookie);
          removedCount++;
        } catch (error) {
          console.error(`Failed to remove cookie ${existingCookie.name}:`, error);
        }
      }

      await new Promise((resolve) => setTimeout(resolve, 200));

      console.log(`Adding ${newCookies.length} new cookies...`);
      if (!silentMode) {
        showNotification(`Adding ${newCookies.length} new cookies...`, 'info');
      }

      for (const cookie of newCookies) {
        try {
          await saveCookie(cookie, currentUrl);
          successCount++;
        } catch (error) {
          errorCount++;
          console.error(`Failed to process cookie ${cookie.name}:`, error);
        }
      }

      if (!silentMode) {
        const message = `Removed ${removedCount} old cookies, added ${successCount} new cookies${errorCount > 0 ? `, ${errorCount} failed` : ''}`;
        showNotification(message, errorCount > 0 ? 'error' : 'success');
      }

      if (successCount > 0) {
        setTimeout(() => {
          window.location.reload();
        }, 1000);
      }

      return {
        success: true,
        stats: { total: newCookies.length, success: successCount, error: errorCount },
      };
    } catch (error) {
      console.error('Error updating cookies:', error);
      if (!silentMode) {
        showNotification('Failed to update cookies: ' + error.message, 'error');
      }
      return { success: false, error: error.message };
    }
  }

  // =====================================================
  // COURSE DETECTION
  // =====================================================
  function getCurrentCourseInfo() {
    const url = window.location.href;
    let courseId = null;
    let courseTitle = null;
    let courseImage = null;
    const courseUrl = url;
    let instructor = null;

    const courseMatch = url.match(/\/course\/([^/?]+)/);
    if (courseMatch) {
      courseId = courseMatch[1];
    }

    const titleEl = document.querySelector(
      '[data-purpose="course-title"], h1.ud-heading-xl, h1.clp-lead__title, .ud-heading-xxl'
    );
    if (titleEl) {
      courseTitle = titleEl.textContent.trim();
    }

    // Try multiple selectors for course image
    const imgSelectors = [
      '[data-purpose="course-image"] img',
      '.intro-asset--img-aspect--1UbeZ img',
      '.course-image img',
      'img[src*="img-c.udemycdn.com/course"]',
      'img[src*="udemycdn.com/course"]',
    ];

    for (const selector of imgSelectors) {
      const imgEl = document.querySelector(selector);
      if (imgEl && imgEl.src) {
        courseImage = imgEl.src;
        break;
      }
    }

    // Fallback: find any large course-related image
    if (!courseImage) {
      const allImages = document.querySelectorAll('img[src*="udemycdn.com"]');
      for (const img of allImages) {
        // Look for course images (usually 480x270 or larger)
        if (
          img.src.includes('/course/') &&
          !img.src.includes('icon') &&
          !img.src.includes('avatar')
        ) {
          courseImage = img.src;
          break;
        }
      }
    }

    const instructorEl = document.querySelector(
      '[data-purpose="instructor-name-top"], .ud-instructor-links a, .instructor-links a'
    );
    if (instructorEl) {
      instructor = instructorEl.textContent.trim();
    }

    if (!courseTitle) {
      courseTitle = document.title.replace(' | Udemy Business', '').replace(' | Udemy', '').trim();
    }

    return {
      id: courseId || btoa(url).slice(0, 20),
      title: courseTitle || 'Unknown Course',
      image: courseImage,
      url: courseUrl,
      instructor: instructor,
      addedAt: Date.now(),
    };
  }

  // =====================================================
  // STYLES
  // =====================================================
  function injectStyles() {
    if (document.getElementById('udemy-combined-styles')) return;

    const styles = document.createElement('style');
    styles.id = 'udemy-combined-styles';
    styles.textContent = `
            @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap');

            #udemy-cookie-notification {
                position: fixed;
                top: 20px;
                right: 20px;
                padding: 15px 20px;
                border-radius: 10px;
                color: white;
                font-family: 'Plus Jakarta Sans', Arial, sans-serif;
                font-size: 14px;
                z-index: 100002;
                max-width: 300px;
                word-wrap: break-word;
                box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
                transition: opacity 0.3s ease;
            }

            #udemy-combined-controls {
                position: fixed;
                bottom: 20px;
                right: 20px;
                display: flex;
                flex-direction: column;
                align-items: flex-end;
                gap: 10px;
                z-index: 99990;
                font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, sans-serif;
            }

            .ucc-btn {
                padding: 12px;
                border: none;
                border-radius: 12px;
                cursor: pointer;
                font-size: 13px;
                font-weight: 600;
                display: flex;
                align-items: center;
                gap: 0;
                transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
                box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
                font-family: inherit;
                overflow: hidden;
                white-space: nowrap;
            }

            .ucc-btn svg {
                width: 18px;
                height: 18px;
                flex-shrink: 0;
            }

            .ucc-btn .ucc-btn-text {
                max-width: 0;
                opacity: 0;
                overflow: hidden;
                transition: max-width 0.3s cubic-bezier(0.4, 0, 0.2, 1), 
                            opacity 0.2s ease 0.1s,
                            margin-left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
                margin-left: 0;
            }

            .ucc-btn:hover {
                padding: 12px 16px;
                transform: translateY(-2px);
                box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
            }

            .ucc-btn:hover .ucc-btn-text {
                max-width: 120px;
                opacity: 1;
                margin-left: 8px;
            }

            .ucc-btn:disabled {
                opacity: 0.6;
                cursor: not-allowed;
                transform: none;
            }

            .ucc-btn.primary { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; }
            .ucc-btn.secondary { background: #1f2937; color: white; }
            .ucc-btn.success { background: linear-gradient(135deg, #10b981 0%, #059669 100%); color: white; }

            .ufo-overlay {
                position: fixed;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                background: rgba(0, 0, 0, 0.7);
                backdrop-filter: blur(8px);
                z-index: 99998;
                opacity: 0;
                transition: opacity 0.3s ease;
            }

            .ufo-overlay.visible { opacity: 1; }

            .ufo-popup {
                position: fixed;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%) scale(0.9);
                width: 900px;
                max-width: 95vw;
                height: 650px;
                max-height: 90vh;
                background: linear-gradient(145deg, #1a1a2e 0%, #16213e 100%);
                border-radius: 20px;
                box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.1);
                z-index: 99999;
                font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, sans-serif;
                display: flex;
                flex-direction: column;
                overflow: hidden;
                opacity: 0;
                transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
            }

            .ufo-popup.visible { opacity: 1; transform: translate(-50%, -50%) scale(1); }

            .ufo-header {
                padding: 20px 24px;
                background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
                display: flex;
                justify-content: space-between;
                align-items: center;
            }

            .ufo-header h2 {
                margin: 0;
                font-size: 20px;
                font-weight: 700;
                color: white;
                display: flex;
                align-items: center;
                gap: 10px;
            }

            .ufo-header-icon {
                width: 28px;
                height: 28px;
                background: rgba(255, 255, 255, 0.2);
                border-radius: 8px;
                display: flex;
                align-items: center;
                justify-content: center;
            }

            .ufo-header-right { display: flex; align-items: center; gap: 12px; }
            .ufo-user-info { font-size: 12px; color: rgba(255, 255, 255, 0.8); text-align: right; }
            .ufo-user-info span { display: block; }

            .ufo-sync-btn {
                padding: 8px 12px;
                background: rgba(255, 255, 255, 0.15);
                border: none;
                border-radius: 8px;
                color: white;
                cursor: pointer;
                display: flex;
                align-items: center;
                gap: 6px;
                font-size: 12px;
                transition: all 0.2s ease;
            }

            .ufo-sync-btn:hover { background: rgba(255, 255, 255, 0.25); }
            .ufo-sync-btn.syncing svg { animation: spin 1s linear infinite; }
            @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }

            .ufo-close-btn {
                width: 36px;
                height: 36px;
                border: none;
                background: rgba(255, 255, 255, 0.15);
                color: white;
                border-radius: 10px;
                cursor: pointer;
                display: flex;
                align-items: center;
                justify-content: center;
                transition: all 0.2s ease;
            }

            .ufo-close-btn:hover { background: rgba(255, 255, 255, 0.25); transform: rotate(90deg); }

            .ufo-body { display: flex; flex: 1; overflow: hidden; min-height: 0; }

            .ufo-sidebar {
                width: 280px;
                background: rgba(0, 0, 0, 0.2);
                border-right: 1px solid rgba(255, 255, 255, 0.08);
                display: flex;
                flex-direction: column;
            }

            .ufo-sidebar-header { padding: 16px; border-bottom: 1px solid rgba(255, 255, 255, 0.08); }

            .ufo-new-folder-btn {
                width: 100%;
                padding: 12px 16px;
                background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
                color: white;
                border: none;
                border-radius: 10px;
                font-size: 14px;
                font-weight: 600;
                cursor: pointer;
                display: flex;
                align-items: center;
                justify-content: center;
                gap: 8px;
                transition: all 0.2s ease;
                font-family: inherit;
            }

            .ufo-new-folder-btn:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4); }

            .ufo-folder-list { flex: 1; overflow-y: auto; padding: 12px; }

            .ufo-folder-item {
                padding: 12px 14px;
                border-radius: 10px;
                cursor: pointer;
                display: flex;
                align-items: center;
                gap: 12px;
                margin-bottom: 6px;
                transition: all 0.2s ease;
                position: relative;
            }

            .ufo-folder-item:hover { background: rgba(255, 255, 255, 0.08); }
            .ufo-folder-item.active { background: rgba(102, 126, 234, 0.2); }

            .ufo-folder-icon {
                width: 36px;
                height: 36px;
                border-radius: 8px;
                display: flex;
                align-items: center;
                justify-content: center;
                font-size: 18px;
                flex-shrink: 0;
            }

            .ufo-folder-info { flex: 1; min-width: 0; }
            .ufo-folder-name { color: #fff; font-size: 14px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
            .ufo-folder-count { color: rgba(255, 255, 255, 0.5); font-size: 12px; margin-top: 2px; }

            .ufo-folder-menu-btn {
                width: 28px;
                height: 28px;
                border: none;
                background: transparent;
                color: rgba(255, 255, 255, 0.4);
                border-radius: 6px;
                cursor: pointer;
                opacity: 0;
                transition: all 0.2s ease;
                display: flex;
                align-items: center;
                justify-content: center;
            }

            .ufo-folder-item:hover .ufo-folder-menu-btn { opacity: 1; }
            .ufo-folder-menu-btn:hover { background: rgba(255, 255, 255, 0.1); color: white; }

            .ufo-content { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-height: 0; }

            .ufo-content-header {
                padding: 20px 24px;
                border-bottom: 1px solid rgba(255, 255, 255, 0.08);
                display: flex;
                justify-content: space-between;
                align-items: center;
            }

            .ufo-content-title { color: white; font-size: 18px; font-weight: 700; display: flex; align-items: center; gap: 10px; }
            .ufo-search-box { position: relative; }

            .ufo-search-input {
                width: 220px;
                padding: 10px 14px 10px 38px;
                background: rgba(255, 255, 255, 0.08);
                border: 1px solid rgba(255, 255, 255, 0.12);
                border-radius: 10px;
                color: white;
                font-size: 13px;
                font-family: inherit;
                transition: all 0.2s ease;
            }

            .ufo-search-input::placeholder { color: rgba(255, 255, 255, 0.4); }
            .ufo-search-input:focus { outline: none; background: rgba(255, 255, 255, 0.12); border-color: rgba(102, 126, 234, 0.5); }
            .ufo-search-icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: rgba(255, 255, 255, 0.4); }

            .ufo-course-grid {
                flex: 1;
                overflow-y: auto;
                padding: 20px 24px;
                display: grid;
                grid-template-columns: repeat(2, 1fr);
                grid-auto-rows: min-content;
                gap: 16px;
                min-height: 0;
                align-content: start;
            }
            
            .ufo-pagination {
                display: flex;
                align-items: center;
                justify-content: center;
                gap: 16px;
                padding: 16px 24px;
                border-top: 1px solid rgba(255, 255, 255, 0.08);
                background: rgba(0, 0, 0, 0.2);
            }
            
            .ufo-pagination-btn {
                padding: 10px 20px;
                background: rgba(255, 255, 255, 0.1);
                border: 1px solid rgba(255, 255, 255, 0.15);
                border-radius: 8px;
                color: white;
                font-size: 13px;
                font-weight: 600;
                cursor: pointer;
                transition: all 0.2s ease;
                font-family: inherit;
                display: flex;
                align-items: center;
                gap: 6px;
            }
            
            .ufo-pagination-btn:hover:not(:disabled) {
                background: rgba(255, 255, 255, 0.2);
                transform: translateY(-1px);
            }
            
            .ufo-pagination-btn:disabled {
                opacity: 0.4;
                cursor: not-allowed;
            }
            
            .ufo-pagination-info {
                color: rgba(255, 255, 255, 0.7);
                font-size: 13px;
                min-width: 100px;
                text-align: center;
            }

            .ufo-course-card {
                background: rgba(255, 255, 255, 0.05);
                border-radius: 12px;
                overflow: hidden;
                transition: all 0.2s ease;
                border: 1px solid rgba(255, 255, 255, 0.08);
                display: flex;
                flex-direction: column;
                height: auto;
            }

            .ufo-course-card:hover { transform: translateY(-2px); background: rgba(255, 255, 255, 0.08); box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3); }
            
            .ufo-course-image-link {
                display: block;
                cursor: pointer;
                flex-shrink: 0;
            }
            
            .ufo-course-image { 
                width: 100%; 
                height: 120px; 
                object-fit: cover; 
                background: rgba(255, 255, 255, 0.1);
                display: block;
            }
            
            .ufo-course-info { 
                padding: 12px; 
                display: flex; 
                flex-direction: column; 
                gap: 8px;
            }

            .ufo-course-title {
                color: white;
                font-size: 13px;
                font-weight: 600;
                line-height: 1.3;
                display: -webkit-box;
                -webkit-line-clamp: 2;
                -webkit-box-orient: vertical;
                overflow: hidden;
            }

            .ufo-course-instructor { 
                color: rgba(255, 255, 255, 0.5); 
                font-size: 11px; 
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
            }
            
            .ufo-course-actions { 
                display: flex; 
                gap: 6px; 
                margin-top: auto;
            }

            .ufo-course-btn {
                padding: 4px 10px;
                border: none;
                border-radius: 4px;
                font-size: 10px;
                font-weight: 500;
                cursor: pointer;
                transition: all 0.2s ease;
                font-family: inherit;
            }

            .ufo-course-btn.primary { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; }
            .ufo-course-btn.primary:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); }
            .ufo-course-btn.danger { background: rgba(239, 68, 68, 0.2); color: #f87171; }
            .ufo-course-btn.danger:hover { background: rgba(239, 68, 68, 0.3); }

            .ufo-empty-state {
                display: flex;
                flex-direction: column;
                align-items: center;
                justify-content: center;
                height: 100%;
                color: rgba(255, 255, 255, 0.5);
                text-align: center;
                padding: 40px;
            }

            .ufo-empty-icon { font-size: 64px; margin-bottom: 16px; opacity: 0.5; }
            .ufo-empty-text { font-size: 16px; margin-bottom: 8px; }
            .ufo-empty-hint { font-size: 13px; opacity: 0.7; }

            .ufo-loading {
                display: flex;
                align-items: center;
                justify-content: center;
                height: 100%;
                color: rgba(255, 255, 255, 0.5);
            }

            .ufo-loading-spinner {
                width: 40px;
                height: 40px;
                border: 3px solid rgba(255, 255, 255, 0.1);
                border-top-color: #667eea;
                border-radius: 50%;
                animation: spin 1s linear infinite;
            }

            .ufo-modal {
                position: fixed;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%) scale(0.9);
                background: linear-gradient(145deg, #1a1a2e 0%, #16213e 100%);
                padding: 24px;
                border-radius: 16px;
                box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
                z-index: 100000;
                min-width: 360px;
                max-width: 90vw;
                opacity: 0;
                transition: all 0.3s ease;
                font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, sans-serif;
            }

            .ufo-modal.visible { opacity: 1; transform: translate(-50%, -50%) scale(1); }
            .ufo-modal-title { color: white; font-size: 18px; font-weight: 700; margin: 0 0 20px 0; }

            .ufo-modal-input {
                width: 100%;
                padding: 12px 16px;
                background: rgba(255, 255, 255, 0.08);
                border: 1px solid rgba(255, 255, 255, 0.12);
                border-radius: 10px;
                color: white;
                font-size: 14px;
                font-family: inherit;
                margin-bottom: 16px;
                box-sizing: border-box;
            }

            .ufo-modal-input:focus { outline: none; border-color: rgba(102, 126, 234, 0.5); }
            .ufo-color-picker { display: flex; gap: 8px; margin-bottom: 20px; }

            .ufo-color-option {
                width: 32px;
                height: 32px;
                border-radius: 8px;
                cursor: pointer;
                border: 2px solid transparent;
                transition: all 0.2s ease;
            }

            .ufo-color-option:hover { transform: scale(1.1); }
            .ufo-color-option.selected { border-color: white; box-shadow: 0 0 12px rgba(255, 255, 255, 0.3); }

            .ufo-modal-actions { display: flex; gap: 10px; justify-content: flex-end; }

            .ufo-modal-btn {
                padding: 10px 20px;
                border: none;
                border-radius: 8px;
                font-size: 14px;
                font-weight: 600;
                cursor: pointer;
                transition: all 0.2s ease;
                font-family: inherit;
            }

            .ufo-modal-btn.primary { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; }
            .ufo-modal-btn.cancel { background: rgba(255, 255, 255, 0.1); color: rgba(255, 255, 255, 0.7); }
            .ufo-modal-btn:hover { transform: translateY(-2px); }
            .ufo-modal-btn:disabled { opacity: 0.6; cursor: not-allowed; transform: none; }

            .ufo-dropdown {
                position: absolute;
                background: #1e1e2e;
                border-radius: 10px;
                box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
                z-index: 100001;
                min-width: 160px;
                overflow: hidden;
                border: 1px solid rgba(255, 255, 255, 0.1);
            }

            .ufo-dropdown-item {
                padding: 12px 16px;
                color: rgba(255, 255, 255, 0.8);
                font-size: 13px;
                cursor: pointer;
                display: flex;
                align-items: center;
                gap: 10px;
                transition: all 0.2s ease;
            }

            .ufo-dropdown-item:hover { background: rgba(255, 255, 255, 0.1); color: white; }
            .ufo-dropdown-item.danger { color: #f87171; }
            .ufo-dropdown-item.danger:hover { background: rgba(239, 68, 68, 0.2); }

            .ufo-folder-select { margin-bottom: 20px; }
            .ufo-folder-select-label { color: rgba(255, 255, 255, 0.7); font-size: 13px; margin-bottom: 8px; display: block; }
            .ufo-folder-select-options { display: flex; flex-wrap: wrap; gap: 8px; }

            .ufo-folder-select-option {
                padding: 8px 14px;
                background: rgba(255, 255, 255, 0.08);
                border: 1px solid rgba(255, 255, 255, 0.12);
                border-radius: 8px;
                color: rgba(255, 255, 255, 0.8);
                font-size: 13px;
                cursor: pointer;
                transition: all 0.2s ease;
            }

            .ufo-folder-select-option:hover { background: rgba(255, 255, 255, 0.12); }
            .ufo-folder-select-option.selected { background: rgba(102, 126, 234, 0.3); border-color: rgba(102, 126, 234, 0.5); color: white; }

            .ufo-settings-section { margin-bottom: 20px; }
            .ufo-settings-section-title { color: rgba(255, 255, 255, 0.5); font-size: 11px; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 12px; }

            .ufo-settings-row {
                display: flex;
                align-items: center;
                justify-content: space-between;
                padding: 12px 0;
                border-bottom: 1px solid rgba(255, 255, 255, 0.08);
            }

            .ufo-settings-row:last-child { border-bottom: none; }
            .ufo-settings-label { color: white; font-size: 14px; }
            .ufo-settings-hint { color: rgba(255, 255, 255, 0.5); font-size: 12px; margin-top: 4px; }

            .ufo-toggle {
                position: relative;
                width: 44px;
                height: 24px;
                background: rgba(255, 255, 255, 0.2);
                border-radius: 12px;
                cursor: pointer;
                transition: all 0.2s ease;
            }

            .ufo-toggle.active { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }

            .ufo-toggle::after {
                content: '';
                position: absolute;
                top: 2px;
                left: 2px;
                width: 20px;
                height: 20px;
                background: white;
                border-radius: 50%;
                transition: all 0.2s ease;
            }

            .ufo-toggle.active::after { left: 22px; }

            .ufo-scrollbar::-webkit-scrollbar { width: 8px; }
            .ufo-scrollbar::-webkit-scrollbar-track { background: transparent; }
            .ufo-scrollbar::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.2); border-radius: 4px; }
            .ufo-scrollbar::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.3); }
        `;

    document.head.appendChild(styles);
  }

  // =====================================================
  // ICONS
  // =====================================================
  const ICONS = {
    folder: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>`,
    plus: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>`,
    close: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>`,
    search: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><path d="m21 21-4.35-4.35"></path></svg>`,
    more: `<svg viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="6" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="12" cy="18" r="2"/></svg>`,
    edit: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>`,
    trash: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>`,
    external: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>`,
    bookmark: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M17 3H7c-1.1 0-2 .9-2 2v16l7-3 7 3V5c0-1.1-.9-2-2-2z"/></svg>`,
    settings: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>`,
    emptyFolder: `📂`,
    refresh: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>`,
    cloud: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"></path></svg>`,
  };

  // =====================================================
  // NOTIFICATIONS
  // =====================================================
  function showNotification(message, type = 'info') {
    const existingNotification = document.getElementById('udemy-cookie-notification');
    if (existingNotification) existingNotification.remove();

    const notification = document.createElement('div');
    notification.id = 'udemy-cookie-notification';

    let bgColor;
    switch (type) {
      case 'success':
        bgColor = 'linear-gradient(135deg, #10b981 0%, #059669 100%)';
        break;
      case 'error':
        bgColor = 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)';
        break;
      case 'warning':
        bgColor = 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)';
        break;
      default:
        bgColor = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
    }

    notification.style.background = bgColor;
    notification.textContent = message;
    document.body.appendChild(notification);

    setTimeout(() => {
      if (notification.parentNode) {
        notification.style.opacity = '0';
        setTimeout(() => notification.parentNode && notification.remove(), 300);
      }
    }, 3000);
  }

  // =====================================================
  // DROPDOWN
  // =====================================================
  function showDropdown(anchor, items) {
    closeAllDropdowns();

    const rect = anchor.getBoundingClientRect();
    const dropdown = document.createElement('div');
    dropdown.className = 'ufo-dropdown';
    dropdown.style.top = `${rect.bottom + 4}px`;
    dropdown.style.left = `${rect.left}px`;

    items.forEach((item) => {
      const el = document.createElement('div');
      el.className = `ufo-dropdown-item ${item.danger ? 'danger' : ''}`;
      el.innerHTML = `${item.icon || ''} ${item.label}`;
      el.addEventListener('click', (e) => {
        e.stopPropagation();
        closeAllDropdowns();
        item.onClick();
      });
      dropdown.appendChild(el);
    });

    document.body.appendChild(dropdown);
    setTimeout(() => document.addEventListener('click', closeAllDropdowns, { once: true }), 0);
  }

  function closeAllDropdowns() {
    document.querySelectorAll('.ufo-dropdown').forEach((d) => d.remove());
  }

  // =====================================================
  // MODALS
  // =====================================================
  function showCreateFolderModal(callback) {
    const colors = [
      '#6366f1',
      '#ec4899',
      '#f59e0b',
      '#10b981',
      '#3b82f6',
      '#8b5cf6',
      '#ef4444',
      '#06b6d4',
    ];
    let selectedColor = colors[0];

    const overlay = document.createElement('div');
    overlay.className = 'ufo-overlay';
    overlay.addEventListener('click', () => closeModal());

    const modal = document.createElement('div');
    modal.className = 'ufo-modal';
    modal.innerHTML = `
            <h3 class="ufo-modal-title">Create New Folder</h3>
            <input type="text" class="ufo-modal-input" placeholder="Folder name" autofocus>
            <div class="ufo-color-picker">
                ${colors.map((c, i) => `<div class="ufo-color-option ${i === 0 ? 'selected' : ''}" data-color="${c}" style="background: ${c}"></div>`).join('')}
            </div>
            <div class="ufo-modal-actions">
                <button class="ufo-modal-btn cancel">Cancel</button>
                <button class="ufo-modal-btn primary">Create</button>
            </div>
        `;

    document.body.appendChild(overlay);
    document.body.appendChild(modal);

    setTimeout(() => {
      overlay.classList.add('visible');
      modal.classList.add('visible');
      modal.querySelector('input').focus();
    }, 10);

    modal.querySelectorAll('.ufo-color-option').forEach((opt) => {
      opt.addEventListener('click', () => {
        modal.querySelectorAll('.ufo-color-option').forEach((o) => o.classList.remove('selected'));
        opt.classList.add('selected');
        selectedColor = opt.dataset.color;
      });
    });

    const closeModal = () => {
      overlay.classList.remove('visible');
      modal.classList.remove('visible');
      setTimeout(() => {
        overlay.remove();
        modal.remove();
      }, 300);
    };

    modal.querySelector('.cancel').addEventListener('click', closeModal);
    modal.querySelector('.primary').addEventListener('click', async () => {
      const name = modal.querySelector('input').value.trim();
      if (name) {
        modal.querySelector('.primary').disabled = true;
        modal.querySelector('.primary').textContent = 'Creating...';
        await callback(name, selectedColor);
        closeModal();
      }
    });

    modal.querySelector('input').addEventListener('keydown', async (e) => {
      if (e.key === 'Enter') {
        const name = modal.querySelector('input').value.trim();
        if (name) {
          modal.querySelector('.primary').disabled = true;
          await callback(name, selectedColor);
          closeModal();
        }
      } else if (e.key === 'Escape') {
        closeModal();
      }
    });
  }

  function showRenameFolderModal(currentName, folderId, callback) {
    const overlay = document.createElement('div');
    overlay.className = 'ufo-overlay';
    overlay.addEventListener('click', () => closeModal());

    const modal = document.createElement('div');
    modal.className = 'ufo-modal';
    modal.innerHTML = `
            <h3 class="ufo-modal-title">Rename Folder</h3>
            <input type="text" class="ufo-modal-input" value="${currentName}" autofocus>
            <div class="ufo-modal-actions">
                <button class="ufo-modal-btn cancel">Cancel</button>
                <button class="ufo-modal-btn primary">Rename</button>
            </div>
        `;

    document.body.appendChild(overlay);
    document.body.appendChild(modal);

    setTimeout(() => {
      overlay.classList.add('visible');
      modal.classList.add('visible');
      modal.querySelector('input').select();
    }, 10);

    const closeModal = () => {
      overlay.classList.remove('visible');
      modal.classList.remove('visible');
      setTimeout(() => {
        overlay.remove();
        modal.remove();
      }, 300);
    };

    modal.querySelector('.cancel').addEventListener('click', closeModal);
    modal.querySelector('.primary').addEventListener('click', async () => {
      const name = modal.querySelector('input').value.trim();
      if (name && name !== currentName) {
        modal.querySelector('.primary').disabled = true;
        modal.querySelector('.primary').textContent = 'Renaming...';
        await callback(name, folderId);
        closeModal();
      }
    });

    modal.querySelector('input').addEventListener('keydown', async (e) => {
      if (e.key === 'Enter') {
        const name = modal.querySelector('input').value.trim();
        if (name && name !== currentName) {
          modal.querySelector('.primary').disabled = true;
          await callback(name, folderId);
          closeModal();
        }
      } else if (e.key === 'Escape') {
        closeModal();
      }
    });
  }

  function showAddCourseModal(courseInfo) {
    const overlay = document.createElement('div');
    overlay.className = 'ufo-overlay';
    overlay.addEventListener('click', () => closeModal());

    const selectedFolderIds = new Set();

    const modal = document.createElement('div');
    modal.className = 'ufo-modal';
    modal.innerHTML = `
            <h3 class="ufo-modal-title">Save Course to Folder</h3>
            <div style="background: rgba(255,255,255,0.05); border-radius: 10px; padding: 12px; margin-bottom: 16px; display: flex; gap: 12px; align-items: center;">
                ${courseInfo.image ? `<img src="${courseInfo.image}" style="width: 80px; height: 45px; border-radius: 6px; object-fit: cover;">` : ''}
                <div style="flex: 1; min-width: 0;">
                    <div style="color: white; font-size: 14px; font-weight: 600; line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${courseInfo.title}</div>
                    ${courseInfo.instructor ? `<div style="color: rgba(255,255,255,0.5); font-size: 12px; margin-top: 4px;">${courseInfo.instructor}</div>` : ''}
                </div>
            </div>
            <div class="ufo-folder-select">
                <label class="ufo-folder-select-label">Select folders:</label>
                <div class="ufo-folder-select-options">
                    ${folders
                      .map(
                        (f) => `
                        <div class="ufo-folder-select-option" data-folder-id="${f.id}">
                            <span style="display: inline-block; width: 10px; height: 10px; border-radius: 3px; background: ${f.color}; margin-right: 6px;"></span>
                            ${f.name}
                        </div>
                    `
                      )
                      .join('')}
                </div>
            </div>
            <div class="ufo-modal-actions">
                <button class="ufo-modal-btn cancel">Cancel</button>
                <button class="ufo-modal-btn primary">Save</button>
            </div>
        `;

    document.body.appendChild(overlay);
    document.body.appendChild(modal);

    setTimeout(() => {
      overlay.classList.add('visible');
      modal.classList.add('visible');
    }, 10);

    modal.querySelectorAll('.ufo-folder-select-option').forEach((opt) => {
      opt.addEventListener('click', () => {
        const folderId = parseInt(opt.dataset.folderId);
        if (selectedFolderIds.has(folderId)) {
          selectedFolderIds.delete(folderId);
          opt.classList.remove('selected');
        } else {
          selectedFolderIds.add(folderId);
          opt.classList.add('selected');
        }
      });
    });

    const closeModal = () => {
      overlay.classList.remove('visible');
      modal.classList.remove('visible');
      setTimeout(() => {
        overlay.remove();
        modal.remove();
      }, 300);
    };

    modal.querySelector('.cancel').addEventListener('click', closeModal);
    modal.querySelector('.primary').addEventListener('click', async () => {
      if (selectedFolderIds.size > 0) {
        modal.querySelector('.primary').disabled = true;
        modal.querySelector('.primary').textContent = 'Saving...';

        try {
          await addCourseToFoldersAPI(Array.from(selectedFolderIds), courseInfo);
          closeModal();
          showNotification(`Course saved to ${selectedFolderIds.size} folder(s)!`, 'success');
        } catch (error) {
          showNotification('Failed to save course: ' + error.message, 'error');
          modal.querySelector('.primary').disabled = false;
          modal.querySelector('.primary').textContent = 'Save';
        }
      }
    });
  }

  function showSettingsModal() {
    const overlay = document.createElement('div');
    overlay.className = 'ufo-overlay';
    overlay.addEventListener('click', () => closeModal());

    const userInfo = getUserInfo();

    const modal = document.createElement('div');
    modal.className = 'ufo-modal';
    modal.style.minWidth = '450px';
    modal.innerHTML = `            
            <div class="ufo-settings-section">
                <div class="ufo-settings-section-title">Account</div>
                <div class="ufo-settings-row">
                    <div>
                        <div class="ufo-settings-label">License Key</div>
                        <div class="ufo-settings-hint">${userInfo.licenseKey}</div>
                    </div>
                </div>
                <div class="ufo-settings-row">
                    <div>
                        <div class="ufo-settings-label">Device ID</div>
                        <div class="ufo-settings-hint">${userInfo.deviceId}</div>
                    </div>
                </div>
                <div class="ufo-settings-row">
                    <div>
                        <div class="ufo-settings-label">Cloud Sync</div>
                        <div class="ufo-settings-hint">${config.licenseKey ? 'Enabled' : 'Disabled (set license key)'}</div>
                    </div>
                </div>
            </div>

            <div class="ufo-settings-section">
                <div class="ufo-settings-section-title">Configuration</div>
                <div style="margin-bottom: 12px;">
                    <label style="color: rgba(255,255,255,0.7); font-size: 12px; display: block; margin-bottom: 6px;">License Key</label>
                    <input type="text" class="ufo-modal-input" id="settings-license-key" value="${config.licenseKey}" style="margin-bottom: 0;">
                </div>
            </div>

            <div class="ufo-settings-section">
                <div class="ufo-settings-section-title">Display</div>
                <div class="ufo-settings-row">
                    <div class="ufo-settings-label">Show UI Buttons</div>
                    <div class="ufo-toggle ${config.showUiButtons ? 'active' : ''}" data-setting="showUiButtons"></div>
                </div>
                <div class="ufo-settings-row">
                    <div class="ufo-settings-label">Show Folder Organizer</div>
                    <div class="ufo-toggle ${config.showFolderOrganizer ? 'active' : ''}" data-setting="showFolderOrganizer"></div>
                </div>
            </div>

            <div class="ufo-settings-section">
                <div class="ufo-settings-section-title">Statistics</div>
                <div class="ufo-settings-row">
                    <div class="ufo-settings-label">Total Folders</div>
                    <div style="color: rgba(255,255,255,0.7);">${userInfo.totalFolders}</div>
                </div>
                <div class="ufo-settings-row">
                    <div class="ufo-settings-label">Total Saved Courses</div>
                    <div style="color: rgba(255,255,255,0.7);">${userInfo.totalCourses}</div>
                </div>
            </div>

            <div class="ufo-modal-actions">
                <button class="ufo-modal-btn cancel">Cancel</button>
                <button class="ufo-modal-btn primary">Save Settings</button>
            </div>
        `;

    document.body.appendChild(overlay);
    document.body.appendChild(modal);

    setTimeout(() => {
      overlay.classList.add('visible');
      modal.classList.add('visible');
    }, 10);

    modal.querySelectorAll('.ufo-toggle').forEach((toggle) => {
      toggle.addEventListener('click', () => toggle.classList.toggle('active'));
    });

    const closeModal = () => {
      overlay.classList.remove('visible');
      modal.classList.remove('visible');
      setTimeout(() => {
        overlay.remove();
        modal.remove();
      }, 300);
    };

    modal.querySelector('.cancel').addEventListener('click', closeModal);
    modal.querySelector('.primary').addEventListener('click', async () => {
      const oldLicenseKey = config.licenseKey;

      config.licenseKey = modal.querySelector('#settings-license-key').value;

      config.showUiButtons = modal
        .querySelector('[data-setting="showUiButtons"]')
        .classList.contains('active');
      config.showFolderOrganizer = modal
        .querySelector('[data-setting="showFolderOrganizer"]')
        .classList.contains('active');

      saveConfig();
      closeModal();
      showNotification('Settings saved!', 'success');
      renderFloatingControls();

      // If license key changed, sync folders
      if (config.licenseKey && config.licenseKey !== oldLicenseKey) {
        await initDefaultFolders();
        await syncFoldersFromServer();
      }
    });
  }

  // =====================================================
  // MAIN POPUP (FOLDER ORGANIZER)
  // =====================================================
  let currentFolderId = null;
  let searchQuery = '';
  let currentPage = 1;
  const ITEMS_PER_PAGE = 4;

  async function createMainPopup() {
    if (document.getElementById('ufo-popup')) return;

    const overlay = document.createElement('div');
    overlay.className = 'ufo-overlay';
    overlay.id = 'ufo-overlay';
    overlay.addEventListener('click', closeMainPopup);

    const popup = document.createElement('div');
    popup.className = 'ufo-popup';
    popup.id = 'ufo-popup';

    const userInfo = getUserInfo();

    popup.innerHTML = `
            <div class="ufo-header">
                <h2>
                    <div class="ufo-header-icon">${ICONS.bookmark}</div>
                    Course Folder Organizer
                </h2>
                <div class="ufo-header-right">
                    <div class="ufo-user-info">
                        <span>License: ${userInfo.licenseKey}</span>
                        <span>${userInfo.totalCourses} courses in ${userInfo.totalFolders} folders</span>
                    </div>
                    <button class="ufo-sync-btn" id="ufo-sync-btn" title="Sync with cloud">
                        ${ICONS.refresh} Sync
                    </button>
                    <button class="ufo-close-btn">${ICONS.close}</button>
                </div>
            </div>
            <div class="ufo-body">
                <div class="ufo-sidebar">
                    <div class="ufo-sidebar-header">
                        <button class="ufo-new-folder-btn">
                            ${ICONS.plus} New Folder
                        </button>
                    </div>
                    <div class="ufo-folder-list ufo-scrollbar" id="ufo-folder-list"></div>
                </div>
                <div class="ufo-content">
                    <div class="ufo-content-header">
                        <div class="ufo-content-title" id="ufo-content-title">
                            ${ICONS.folder} All Courses
                        </div>
                        <div class="ufo-search-box">
                            <span class="ufo-search-icon">${ICONS.search}</span>
                            <input type="text" class="ufo-search-input" placeholder="Search courses..." id="ufo-search">
                        </div>
                    </div>
                    <div class="ufo-course-grid" id="ufo-course-grid"></div>
                    <div class="ufo-pagination" id="ufo-pagination">
                        <button class="ufo-pagination-btn" id="ufo-prev-btn">← Previous</button>
                        <span class="ufo-pagination-info" id="ufo-page-info">Page 1 of 1</span>
                        <button class="ufo-pagination-btn" id="ufo-next-btn">Next →</button>
                    </div>
                </div>
            </div>
        `;

    document.body.appendChild(overlay);
    document.body.appendChild(popup);

    popup.querySelector('.ufo-close-btn').addEventListener('click', closeMainPopup);

    popup.querySelector('#ufo-sync-btn').addEventListener('click', async () => {
      const btn = popup.querySelector('#ufo-sync-btn');
      btn.classList.add('syncing');
      btn.disabled = true;

      await syncFoldersFromServer();
      renderFolderList();
      await renderCourseGrid();

      btn.classList.remove('syncing');
      btn.disabled = false;
      showNotification('Synced with cloud!', 'success');
    });

    popup.querySelector('.ufo-new-folder-btn').addEventListener('click', () => {
      showCreateFolderModal(async (name, color) => {
        try {
          await createFolderAPI(name, color);
          renderFolderList();
          showNotification(`Folder "${name}" created!`, 'success');
        } catch (error) {
          showNotification('Failed to create folder: ' + error.message, 'error');
        }
      });
    });

    popup.querySelector('#ufo-search').addEventListener('input', (e) => {
      searchQuery = e.target.value.toLowerCase();
      currentPage = 1; // Reset to first page on search
      renderCourseGrid();
    });

    popup.querySelector('#ufo-prev-btn').addEventListener('click', () => {
      if (currentPage > 1) {
        currentPage--;
        renderCourseGrid();
      }
    });

    popup.querySelector('#ufo-next-btn').addEventListener('click', () => {
      currentPage++;
      renderCourseGrid();
    });

    setTimeout(() => {
      overlay.classList.add('visible');
      popup.classList.add('visible');
    }, 10);

    currentFolderId = null;
    currentPage = 1; // Reset to first page when opening
    renderFolderList();
    await renderCourseGrid();
    isOrganizerPopupOpen = true;
  }

  function closeMainPopup() {
    const overlay = document.getElementById('ufo-overlay');
    const popup = document.getElementById('ufo-popup');

    if (overlay) overlay.classList.remove('visible');
    if (popup) popup.classList.remove('visible');

    setTimeout(() => {
      overlay?.remove();
      popup?.remove();
    }, 300);

    isOrganizerPopupOpen = false;
  }

  function renderFolderList() {
    const container = document.getElementById('ufo-folder-list');
    if (!container) return;

    container.innerHTML = folders
      .map(
        (folder) => `
            <div class="ufo-folder-item ${currentFolderId === folder.id ? 'active' : ''}" data-folder-id="${folder.id}">
                <div class="ufo-folder-icon" style="background: ${folder.color}20; color: ${folder.color}">
                    ${ICONS.folder}
                </div>
                <div class="ufo-folder-info">
                    <div class="ufo-folder-name">${folder.name}</div>
                    <div class="ufo-folder-count">${folder.courses?.length || folder.course_count || 0} courses</div>
                </div>
                <button class="ufo-folder-menu-btn" data-folder-id="${folder.id}">${ICONS.more}</button>
            </div>
        `
      )
      .join('');

    container.querySelectorAll('.ufo-folder-item').forEach((item) => {
      item.addEventListener('click', async (e) => {
        if (e.target.closest('.ufo-folder-menu-btn')) return;
        currentFolderId = parseInt(item.dataset.folderId);
        currentPage = 1; // Reset to first page when switching folders
        renderFolderList();
        await renderCourseGrid();
      });
    });

    container.querySelectorAll('.ufo-folder-menu-btn').forEach((btn) => {
      btn.addEventListener('click', (e) => {
        e.stopPropagation();
        const folderId = parseInt(btn.dataset.folderId);
        const folder = folders.find((f) => f.id === folderId);

        showDropdown(btn, [
          {
            icon: ICONS.edit,
            label: 'Rename',
            onClick: () => {
              showRenameFolderModal(folder.name, folderId, async (newName) => {
                try {
                  await updateFolderAPI(folderId, { name: newName });
                  renderFolderList();
                  showNotification('Folder renamed!', 'success');
                } catch (error) {
                  showNotification('Failed to rename: ' + error.message, 'error');
                }
              });
            },
          },
          {
            icon: ICONS.trash,
            label: 'Delete',
            danger: true,
            onClick: async () => {
              if (confirm(`Delete folder "${folder.name}" and remove all courses from it?`)) {
                try {
                  await deleteFolderAPI(folderId);
                  if (currentFolderId === folderId) currentFolderId = null;
                  renderFolderList();
                  await renderCourseGrid();
                  showNotification('Folder deleted!', 'success');
                } catch (error) {
                  showNotification('Failed to delete: ' + error.message, 'error');
                }
              }
            },
          },
        ]);
      });
    });
  }

  async function renderCourseGrid() {
    const container = document.getElementById('ufo-course-grid');
    const titleEl = document.getElementById('ufo-content-title');
    if (!container || !titleEl) return;

    let courses = [];
    let title = 'All Courses';

    if (currentFolderId) {
      const folder = folders.find((f) => f.id === currentFolderId);
      if (folder) {
        title = folder.name;
        titleEl.innerHTML = `<span style="display: inline-block; width: 16px; height: 16px; border-radius: 4px; background: ${folder.color}; margin-right: 8px;"></span> ${title}`;

        // Load courses for this folder
        if (!folder.courses || folder.courses.length === 0) {
          container.innerHTML = `<div class="ufo-loading" style="grid-column: 1 / -1;"><div class="ufo-loading-spinner"></div></div>`;
          courses = await loadCoursesForFolder(currentFolderId);
          folder.courses = courses;
        } else {
          courses = folder.courses;
        }
      }
    } else {
      titleEl.innerHTML = `${ICONS.folder} All Courses`;
      // Collect all courses from all folders
      const seen = new Set();
      for (const folder of folders) {
        const folderCourses = folder.courses || [];
        for (const course of folderCourses) {
          const courseKey = course.udemy_course_id || course.course_id || course.id;
          if (!seen.has(courseKey)) {
            seen.add(courseKey);
            courses.push(course);
          }
        }
      }
    }

    // Filter by search
    if (searchQuery) {
      courses = courses.filter(
        (c) =>
          (c.title && c.title.toLowerCase().includes(searchQuery)) ||
          (c.instructor && c.instructor.toLowerCase().includes(searchQuery))
      );
    }

    // Pagination calculations
    const totalCourses = courses.length;
    const totalPages = Math.ceil(totalCourses / ITEMS_PER_PAGE);

    // Ensure currentPage is valid
    if (currentPage > totalPages) currentPage = totalPages;
    if (currentPage < 1) currentPage = 1;

    // Update pagination controls
    const paginationEl = document.getElementById('ufo-pagination');
    const prevBtn = document.getElementById('ufo-prev-btn');
    const nextBtn = document.getElementById('ufo-next-btn');
    const pageInfo = document.getElementById('ufo-page-info');

    if (paginationEl && prevBtn && nextBtn && pageInfo) {
      if (totalCourses === 0 || totalPages <= 1) {
        paginationEl.style.display = 'none';
      } else {
        paginationEl.style.display = 'flex';
        prevBtn.disabled = currentPage <= 1;
        nextBtn.disabled = currentPage >= totalPages;
        pageInfo.textContent = `Page ${currentPage} of ${totalPages} (${totalCourses} courses)`;
      }
    }

    if (courses.length === 0) {
      container.innerHTML = `
                <div class="ufo-empty-state" style="grid-column: 1 / -1;">
                    <div class="ufo-empty-icon">${ICONS.emptyFolder}</div>
                    <div class="ufo-empty-text">${searchQuery ? 'No courses found' : 'No courses yet'}</div>
                    <div class="ufo-empty-hint">${searchQuery ? 'Try a different search term' : 'Add courses from any Udemy course page'}</div>
                </div>
            `;
      return;
    }

    // Get courses for current page
    const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
    const endIndex = startIndex + ITEMS_PER_PAGE;
    const pageCourses = courses.slice(startIndex, endIndex);

    // Pre-encoded placeholder image (dark background with folder icon)
    const PLACEHOLDER_IMAGE =
      'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0ODAgMjcwIj48cmVjdCBmaWxsPSIjMWExYTJlIiB3aWR0aD0iNDgwIiBoZWlnaHQ9IjI3MCIvPjxwYXRoIGZpbGw9IiM2NjdlZWEiIGQ9Ik0yODAgMTAwSDIwMGMtNS41IDAtMTAgNC41LTEwIDEwdjgwYzAgNS41IDQuNSAxMCAxMCAxMGgxMjBjNS41IDAgMTAtNC41IDEwLTEwdi02MGMwLTUuNS00LjUtMTAtMTAtMTBoLTYwbC0xMC0yMGMtMi01LTctMTAtMTItMTBoLTM4eiIvPjwvc3ZnPg==';

    container.innerHTML = pageCourses
      .map((course) => {
        // course_id is the database ID, udemy_course_id is the slug
        const dbCourseId = course.course_id || course.id;
        const imageUrl = course.image_url || course.image || PLACEHOLDER_IMAGE;
        const courseUrl = getCourseOpenUrl(course);
        const hasProgress = course.last_lesson_url || (!config.licenseKey && GM_getValue('lessonProgress', {})[course.udemy_course_id || course.course_id]);
        const progressIndicator = hasProgress ? `<span style="color: #10b981; margin-left: 4px;" title="Resume from last lesson">▶</span>` : '';

        return `
                <div class="ufo-course-card" data-course-id="${dbCourseId}">
                    <a href="${courseUrl}" target="_blank" class="ufo-course-image-link" title="${hasProgress ? 'Resume last lesson' : 'Open course'}">
                        <img class="ufo-course-image" src="${imageUrl}" alt="${course.title}" onerror="this.src='${PLACEHOLDER_IMAGE}'">
                    </a>
                    <div class="ufo-course-info">
                        <a href="${courseUrl}" target="_blank" class="ufo-course-title" style="text-decoration: none; cursor: pointer;">${course.title}${progressIndicator}</a>
                        ${course.instructor ? `<div class="ufo-course-instructor">${course.instructor}</div>` : ''}
                        <div class="ufo-course-actions">
                            <button class="ufo-course-btn primary" data-action="open" data-url="${courseUrl}">${hasProgress ? 'Resume' : 'Open'} ${ICONS.external}</button>
                            <button class="ufo-course-btn danger" data-action="remove" data-course-id="${dbCourseId}" data-folder-id="${currentFolderId || ''}">Remove</button>
                        </div>
                    </div>
                </div>
            `;
      })
      .join('');

    container.querySelectorAll('[data-action="open"]').forEach((btn) => {
      btn.addEventListener('click', () => window.open(btn.dataset.url, '_blank'));
    });

    container.querySelectorAll('[data-action="remove"]').forEach((btn) => {
      btn.addEventListener('click', async () => {
        const courseId = parseInt(btn.dataset.courseId) || btn.dataset.courseId;
        const folderId = btn.dataset.folderId ? parseInt(btn.dataset.folderId) : null;

        console.log('Remove clicked - courseId:', courseId, 'folderId:', folderId);

        try {
          if (folderId) {
            // Remove from specific folder
            console.log('Removing course from folder:', folderId, courseId);
            await removeCourseFromFolderAPI(folderId, courseId);
          } else {
            // Remove from all folders
            console.log('Removing course from all folders');
            for (const folder of folders) {
              const hasCourse = folder.courses?.some((c) => {
                const cId = c.course_id || c.id;
                return cId === courseId || cId === String(courseId);
              });
              if (hasCourse) {
                console.log('Removing from folder:', folder.id, folder.name);
                await removeCourseFromFolderAPI(folder.id, courseId);
              }
            }
          }

          renderFolderList();
          await renderCourseGrid();
          showNotification('Course removed!', 'success');
        } catch (error) {
          console.error('Remove error:', error);
          showNotification('Failed to remove course: ' + error.message, 'error');
        }
      });
    });
  }

  // =====================================================
  // COURSE HOVER POPUP - SAVE BUTTON INJECTION
  // =====================================================
  
  // Find the course card that triggered the popup to get the image
  function findCourseCardImage(popupElement, courseUrl) {
    // Strategy 1: Use aria-labelledby to find the trigger element (the course card)
    // The popup wrapper has aria-labelledby pointing to the trigger element
    let triggerElement = null;
    
    // Walk up the popup element to find the wrapper with aria-labelledby
    let current = popupElement;
    while (current && current !== document.body) {
      const ariaLabelledBy = current.getAttribute('aria-labelledby');
      if (ariaLabelledBy) {
        // Found the aria-labelledby, now find the trigger element by ID
        triggerElement = document.getElementById(ariaLabelledBy);
        if (triggerElement) {
          break;
        }
        // Also try querySelector in case it's not a direct ID match
        triggerElement = document.querySelector(`[id="${ariaLabelledBy}"]`);
        if (triggerElement) {
          break;
        }
      }
      current = current.parentElement;
    }
    
    // If found trigger, look for the course card containing it and get the image
    if (triggerElement) {
      // The trigger is usually inside the course card, go up to find the card
      const courseCard = triggerElement.closest('[class*="course-card"]') ||
                         triggerElement.closest('[class*="card--container"]') ||
                         triggerElement.closest('[data-purpose="container"]') ||
                         triggerElement.closest('div[class*="browse-course"]') ||
                         triggerElement.closest('[class*="popper-module--popper"]')?.parentElement ||
                         triggerElement.parentElement?.parentElement?.parentElement;
      
      if (courseCard) {
        const img = courseCard.querySelector('img[src*="udemycdn.com/course"]') ||
                    courseCard.querySelector('img[src*="img-c.udemycdn.com"]') ||
                    courseCard.querySelector('img[class*="course-image"]');
        if (img?.src) {
          return img.src;
        }
      }
      
      // Also check siblings - the image might be in a sibling element
      const parent = triggerElement.parentElement;
      if (parent) {
        const img = parent.querySelector('img[src*="udemycdn.com"]');
        if (img?.src) {
          return img.src;
        }
      }
    }
    
    // Strategy 2: Fall back to URL-based search if aria-labelledby didn't work
    if (!courseUrl) return '';
    
    const slugMatch = courseUrl.match(/\/course\/([^/?]+)/);
    if (!slugMatch) return '';
    const courseSlug = slugMatch[1];
    
    // Find all course cards on the page that link to this course
    const courseLinks = document.querySelectorAll(`a[href*="/course/${courseSlug}"]`);
    
    for (const link of courseLinks) {
      // Skip links inside popups
      if (link.closest('[class*="popover-module"]') || link.closest('[class*="popper-module"]')) {
        continue;
      }
      
      // Look for an image in the same card/container
      const card = link.closest('[class*="course-card"]') || 
                   link.closest('[class*="card--container"]') ||
                   link.closest('[data-purpose="container"]') ||
                   link.closest('div[class*="browse-course"]') ||
                   link.parentElement?.parentElement;
      
      if (card) {
        const img = card.querySelector('img[src*="udemycdn.com/course"]') ||
                    card.querySelector('img[src*="img-c.udemycdn.com"]');
        if (img?.src) {
          return img.src;
        }
      }
    }
    
    return '';
  }
  
  // Find the course card element using aria-labelledby
  function findCourseCard(popupElement) {
    let current = popupElement;
    while (current && current !== document.body) {
      const ariaLabelledBy = current.getAttribute('aria-labelledby');
      if (ariaLabelledBy) {
        const triggerElement = document.getElementById(ariaLabelledBy);
        if (triggerElement) {
          // Find the course card containing this trigger
          const courseCard = triggerElement.closest('[class*="course-card"]') ||
                             triggerElement.closest('[class*="card--container"]') ||
                             triggerElement.closest('[data-purpose="container"]') ||
                             triggerElement.closest('div[class*="browse-course"]') ||
                             triggerElement.closest('[class*="popper-module--popper"]')?.parentElement ||
                             triggerElement.parentElement?.parentElement?.parentElement;
          return courseCard;
        }
      }
      current = current.parentElement;
    }
    return null;
  }

  // Get course info from the course card element
  function getCourseInfoFromCard(cardElement) {
    if (!cardElement) return null;
    
    // Find course link
    const linkEl = cardElement.querySelector('a[href*="/course/"]');
    if (!linkEl) return null;
    
    let url = linkEl.href || '';
    if (url && !url.startsWith('http')) {
      url = 'https://www.udemy.com' + url;
    }
    
    // Extract course ID from URL
    const courseMatch = url.match(/\/course\/([^/?]+)/);
    let courseId = 'unknown';
    if (courseMatch) {
      courseId = courseMatch[1];
    } else if (url) {
      courseId = btoa(url).slice(0, 20);
    }
    
    // Find title - look for heading or link text
    const titleEl = cardElement.querySelector('[data-purpose="course-title-url"]') ||
                    cardElement.querySelector('h3') ||
                    cardElement.querySelector('[class*="course-card--course-title"]') ||
                    linkEl;
    const title = titleEl?.textContent?.trim() || 'Unknown Course';
    
    // Find image
    const imgEl = cardElement.querySelector('img[src*="udemycdn.com"]');
    const image = imgEl?.src || '';
    
    // Find instructor
    const instructorEl = cardElement.querySelector('[data-purpose="safely-set-inner-html:course-card:visible-instructors"]') ||
                         cardElement.querySelector('[class*="course-card--instructor"]');
    const instructor = instructorEl?.textContent?.trim() || '';
    
    return {
      id: courseId,
      title: title,
      image: image,
      url: url,
      instructor: instructor,
      addedAt: Date.now(),
    };
  }

  function getCourseInfoFromPopup(popupElement) {
    // Check if this is a search/objectives popup (doesn't have course title inside)
    const isSearchPopup = popupElement.querySelector('[data-testid="course-objectives-quick-view-box-content"]') ||
                          popupElement.querySelector('[class*="search--quick-view-box"]');
    
    if (isSearchPopup) {
      // Get course info from the course card instead
      const courseCard = findCourseCard(popupElement);
      if (courseCard) {
        const cardInfo = getCourseInfoFromCard(courseCard);
        if (cardInfo) {
          return cardInfo;
        }
      }
    }
    
    // Extract course info from the hover popup (standard popup with title)
    // Try multiple selectors for title - Udemy uses data-testid="quick-view-box-title"
    const titleEl = popupElement.querySelector(
      '[data-testid="quick-view-box-title"]'
    ) || popupElement.querySelector(
      'a[class*="course-details-quick-view-box-module--title"]'
    ) || popupElement.querySelector(
      'a[href*="/course/"]'
    );
    
    let title = titleEl?.textContent?.trim() || '';
    let url = titleEl?.href || '';
    
    // If no title in popup, try to get from course card
    if (!title || title === 'Unknown Course') {
      const courseCard = findCourseCard(popupElement);
      if (courseCard) {
        const cardInfo = getCourseInfoFromCard(courseCard);
        if (cardInfo) {
          return cardInfo;
        }
      }
    }
    
    title = title || 'Unknown Course';
    
    // Make sure we have a full URL
    if (url && !url.startsWith('http')) {
      url = 'https://www.udemy.com' + url;
    }

    // Extract course ID from URL (the slug)
    const courseMatch = url.match(/\/course\/([^/?]+)/);
    let courseId = 'unknown';
    if (courseMatch) {
      courseId = courseMatch[1];
    } else if (url) {
      courseId = btoa(url).slice(0, 20);
    }

    // Find course image - first try in popup, then look for the course card
    let image = '';
    const imgEl = popupElement.querySelector('img[src*="udemycdn.com"]');
    if (imgEl?.src) {
      image = imgEl.src;
    } else {
      // Image not in popup - find it from the course card using aria-labelledby
      image = findCourseCardImage(popupElement, url);
    }

    // Find headline/description as instructor fallback
    const headlineEl = popupElement.querySelector('[data-testid="quick-view-box-headline"]');
    const headline = headlineEl?.textContent?.trim() || '';

    return {
      id: courseId,
      title: title,
      image: image,
      url: url,
      instructor: headline, // Use headline as description/instructor
      addedAt: Date.now(),
    };
  }

  function injectSaveButtonToPopup(popupElement) {
    // Check if we already injected the button anywhere in this popup
    if (popupElement.querySelector('.ufo-popup-save-btn')) return;

    // Check if this is a search/objectives popup (doesn't have CTA area)
    const isSearchPopup = popupElement.querySelector('[data-testid="course-objectives-quick-view-box-content"]') ||
                          popupElement.querySelector('[class*="search--quick-view-box"]');
    
    if (isSearchPopup) {
      // For search popups, add button to the quick-view-box container or popover-inner
      const searchBox = popupElement.querySelector('[data-testid="course-objectives-quick-view-box-content"]') ||
                        popupElement.querySelector('[class*="search--quick-view-box"]');
      const innerContainer = popupElement.querySelector('[class*="popover-module--inner"]');
      
      const targetContainer = searchBox || innerContainer;
      if (targetContainer && !targetContainer.querySelector('.ufo-popup-save-btn')) {
        const saveBtn = createPopupSaveButton(popupElement);
        // Add some top margin for search popup
        saveBtn.style.marginTop = '12px';
        saveBtn.style.marginLeft = '0';
        saveBtn.style.width = '100%';
        saveBtn.style.justifyContent = 'center';
        targetContainer.appendChild(saveBtn);
        return;
      }
    }

    // Find the CTA button placeholder (the empty div next to Enroll button)
    // This is the best place to put our button
    const ctaBtnPlaceholder = popupElement.querySelector('[class*="course-details-quick-view-box-module--cta-button--"]');
    
    if (ctaBtnPlaceholder && !ctaBtnPlaceholder.querySelector('.ufo-popup-save-btn')) {
      const saveBtn = createPopupSaveButton(popupElement);
      ctaBtnPlaceholder.appendChild(saveBtn);
      return;
    }

    // Fallback: Find the CTA container div
    let ctaContainer = popupElement.querySelector('[class*="course-details-quick-view-box-module--cta--"]');
    
    // Fallback: Find the Enroll button and get its parent's parent (the CTA container)
    if (!ctaContainer) {
      const enrollBtn = popupElement.querySelector('[data-testid="enroll-now-button"]');
      if (enrollBtn) {
        ctaContainer = enrollBtn.parentElement?.parentElement;
      }
    }

    if (ctaContainer && !ctaContainer.querySelector('.ufo-popup-save-btn')) {
      const saveBtn = createPopupSaveButton(popupElement);
      ctaContainer.appendChild(saveBtn);
      return;
    }

    // Last fallback: Find the course details content div
    const contentDiv = popupElement.querySelector('[data-testid="course-details-content"]');
    if (contentDiv && !contentDiv.querySelector('.ufo-popup-save-btn')) {
      const saveBtn = createPopupSaveButton(popupElement);
      // Insert after the last child div (before the close button)
      const lastDiv = contentDiv.querySelector(':scope > div');
      if (lastDiv) {
        lastDiv.appendChild(saveBtn);
      } else {
        contentDiv.appendChild(saveBtn);
      }
      return;
    }

    // Final fallback: For any popover with popover-module--inner, add to inner
    const innerDiv = popupElement.querySelector('[class*="popover-module--inner"]');
    if (innerDiv && !innerDiv.querySelector('.ufo-popup-save-btn')) {
      const saveBtn = createPopupSaveButton(popupElement);
      saveBtn.style.marginTop = '12px';
      saveBtn.style.marginLeft = '0';
      saveBtn.style.width = '100%';
      saveBtn.style.justifyContent = 'center';
      innerDiv.appendChild(saveBtn);
    }
  }

  function createPopupSaveButton(popupElement) {
    const saveBtn = document.createElement('button');
    saveBtn.className = 'ufo-popup-save-btn ud-btn ud-btn-medium ud-btn-secondary ud-heading-sm';
    saveBtn.style.cssText = `
      margin-left: 8px;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
      border: none;
      padding: 8px 16px;
      border-radius: 4px;
      cursor: pointer;
      font-weight: 600;
      font-size: 14px;
      display: inline-flex;
      align-items: center;
      gap: 6px;
      transition: all 0.2s ease;
      white-space: nowrap;
    `;
    saveBtn.innerHTML = `<svg viewBox="0 0 24 24" fill="currentColor" style="width:16px;height:16px"><path d="M17 3H7c-1.1 0-2 .9-2 2v16l7-3 7 3V5c0-1.1-.9-2-2-2z"/></svg> Save`;
    saveBtn.title = 'Save course to folder';

    saveBtn.addEventListener('mouseenter', () => {
      saveBtn.style.transform = 'translateY(-2px)';
      saveBtn.style.boxShadow = '0 4px 12px rgba(102, 126, 234, 0.4)';
    });

    saveBtn.addEventListener('mouseleave', () => {
      saveBtn.style.transform = '';
      saveBtn.style.boxShadow = '';
    });

    saveBtn.addEventListener('click', (e) => {
      e.preventDefault();
      e.stopPropagation();
      const courseInfo = getCourseInfoFromPopup(popupElement);
      if (courseInfo.id && courseInfo.title && courseInfo.title !== 'Unknown Course') {
        showAddCourseModal(courseInfo);
      } else {
        showNotification('Could not get course info from popup', 'error');
      }
    });

    return saveBtn;
  }

  function observeCoursePopups() {
    // Selectors for course hover popups - Udemy uses popover-module (not popper)
    const popupSelectors = [
      '[class*="popover-module--popover--"]',           // Main popup container
      '[class*="popper-module--popper-content"]',       // Alternative wrapper
      '[data-testid="popover-render-content"]',         // Data attribute selector
      '[data-testid="course-details-content"]',         // Course details container
      '[data-testid="course-objectives-quick-view-box-content"]', // Search page popup
    ].join(', ');

    // Watch for course hover popups
    const observer = new MutationObserver((mutations) => {
      for (const mutation of mutations) {
        for (const node of mutation.addedNodes) {
          if (node.nodeType !== Node.ELEMENT_NODE) continue;

          // Check if this is a course popup or contains one
          const popups = [];

          // Direct popup detection - check if the added node matches
          if (node.matches && node.matches(popupSelectors)) {
            popups.push(node);
          }

          // Also check children of the added node
          if (node.querySelectorAll) {
            const childPopups = node.querySelectorAll(popupSelectors);
            popups.push(...childPopups);
          }

          // Also check if parent might be a popup (for deeply nested additions)
          let parent = node.parentElement;
          while (parent && parent !== document.body) {
            if (parent.matches && parent.matches(popupSelectors)) {
              if (!popups.includes(parent)) {
                popups.push(parent);
              }
              break;
            }
            parent = parent.parentElement;
          }

          for (const popup of popups) {
            // Check if this popup contains course-related content
            const hasCourseTitle = popup.querySelector('[data-testid="quick-view-box-title"]');
            const hasSearchObjectives = popup.querySelector('[data-testid="course-objectives-quick-view-box-content"]') ||
                                        popup.querySelector('[class*="search--quick-view-box"]');
            
            if (hasCourseTitle || hasSearchObjectives) {
              // Small delay to let the popup fully render
              setTimeout(() => injectSaveButtonToPopup(popup), 150);
            }
          }
        }
      }
    });

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

    // Also try to inject into any existing popups on page load
    setTimeout(() => {
      const existingPopups = document.querySelectorAll(popupSelectors);
      existingPopups.forEach(popup => {
        const hasCourseTitle = popup.querySelector('[data-testid="quick-view-box-title"]');
        if (hasCourseTitle) {
          injectSaveButtonToPopup(popup);
        }
      });
    }, 1000);
  }

  // =====================================================
  // FLOATING CONTROLS
  // =====================================================
  function renderFloatingControls() {
    const existing = document.getElementById('udemy-combined-controls');
    if (existing) existing.remove();

    const container = document.createElement('div');
    container.id = 'udemy-combined-controls';

    const settingsBtn = document.createElement('button');
    settingsBtn.className = 'ucc-btn secondary';
    settingsBtn.innerHTML = `${ICONS.settings}<span class="ucc-btn-text">Settings</span>`;
    settingsBtn.addEventListener('click', showSettingsModal);

    const fetchBtn = document.createElement('button');
    fetchBtn.className = 'ucc-btn secondary';
    fetchBtn.innerHTML = `${ICONS.refresh}<span class="ucc-btn-text">Fetch Cookies</span>`;
    fetchBtn.addEventListener('click', async () => {
      await updateCookiesFromWorker();
    });

    if (config.showUiButtons) {
      container.appendChild(fetchBtn);
      container.appendChild(settingsBtn);
    }

    if (config.showFolderOrganizer) {
      const folderBtn = document.createElement('button');
      folderBtn.className = 'ucc-btn primary';
      folderBtn.innerHTML = `${ICONS.bookmark}<span class="ucc-btn-text">My Folders</span>`;
      folderBtn.addEventListener('click', () => {
        if (isOrganizerPopupOpen) {
          closeMainPopup();
        } else {
          createMainPopup();
        }
      });
      container.appendChild(folderBtn);
    }

    const isCourse = window.location.pathname.includes('/course/');
    if (isCourse && config.showFolderOrganizer) {
      const saveBtn = document.createElement('button');
      saveBtn.className = 'ucc-btn success';
      saveBtn.innerHTML = `${ICONS.plus}<span class="ucc-btn-text">Save Course</span>`;
      saveBtn.addEventListener('click', () => {
        const courseInfo = getCurrentCourseInfo();
        showAddCourseModal(courseInfo);
      });
      container.appendChild(saveBtn);
    }

    document.body.appendChild(container);
  }

  // =====================================================
  // AUTO UPDATE
  // =====================================================
  function startAutoUpdate() {
    const lastUpdate = GM_getValue('lastCookieUpdate', 0);
    const now = Date.now();

    const autoUpdateInterval = 4 * 60 * 60 * 1000; // 4 hours
    if (now - lastUpdate > autoUpdateInterval) {
      updateCookiesFromWorker(true);
      GM_setValue('lastCookieUpdate', now);
    }

    setInterval(() => {
      updateCookiesFromWorker(true);
      GM_setValue('lastCookieUpdate', Date.now());
    }, autoUpdateInterval);
  }

  // =====================================================
  // MENU COMMANDS
  // =====================================================
  function registerMenuCommands() {
    GM_registerMenuCommand('🍪 Update Cookies Now', async () => {
      await updateCookiesFromWorker();
    });
    GM_registerMenuCommand('⚙️ Open Settings', showSettingsModal);
  }

  // =====================================================
  // INITIALIZATION
  // =====================================================
  async function initialize() {
    loadConfig();

    // Load folders from server or local cache
    if (config.licenseKey) {
      await syncFoldersFromServer();
    } else {
      loadFoldersFromLocal();
    }

    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', onDomReady);
    } else {
      onDomReady();
    }

    registerMenuCommands();
    startAutoUpdate();
  }

  function onDomReady() {
    injectStyles();
    renderFloatingControls();
    observeCoursePopups(); // Watch for course hover popups to inject save button
    startLessonTracking(); // Track lesson progress

    let lastUrl = location.href;
    new MutationObserver(() => {
      if (location.href !== lastUrl) {
        lastUrl = location.href;
        setTimeout(renderFloatingControls, 1000);
      }
    }).observe(document.body, { childList: true, subtree: true });
  }

  initialize();
})();