// ==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 // @downloadURL https://update.greasyfork.icu/scripts/547313/Cookie%20Updater.user.js // @updateURL https://update.greasyfork.icu/scripts/547313/Cookie%20Updater.meta.js // ==/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: ``, plus: ``, close: ``, search: ``, more: ``, edit: ``, trash: ``, external: ``, bookmark: ``, settings: ``, emptyFolder: `📂`, refresh: ``, cloud: ``, }; // ===================================================== // 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 = `