diff --git a/CLAUDE.md b/CLAUDE.md index e52e28c..1294003 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,7 +29,9 @@ The extension uses Safari's Manifest V3 architecture with declarative net reques - `manifest.json` - Extension configuration with declarativeNetRequest permissions (Manifest V3) - `rules.json` - Redirect rules using regex patterns to transform YouTube URLs - `background.js` - Service worker for managing rule enable/disable state + - `popup.html/css/js` - Safari toolbar popup UI for user control - `SafariWebExtensionHandler.swift` - Empty placeholder class (legacy Safari App Extension APIs are not used) + - **No content scripts** - All redirects happen at network level via declarativeNetRequest ### URL Redirect Logic diff --git a/FreeYT Extension/Resources/content.js b/FreeYT Extension/Resources/content.js deleted file mode 100644 index 3ddc886..0000000 --- a/FreeYT Extension/Resources/content.js +++ /dev/null @@ -1,149 +0,0 @@ -// FreeYT Content Script -// Redirects YouTube URLs to no-cookie versions by inserting hyphen - -(async function() { - 'use strict'; - - console.log('[FreeYT] Content script loaded on:', window.location.href); - - // Check if extension is enabled - async function isEnabled() { - try { - const result = await chrome.storage.local.get('enabled'); - return result.enabled !== false; // Default to true - } catch (e) { - console.log('[FreeYT] Storage not available, defaulting to enabled'); - return true; - } - } - - // Convert YouTube URL to no-cookie version (youtube → yout-ube) - // Only for actual video URLs, not homepage or browse pages - function convertToNoCookie(url) { - try { - const urlObj = new URL(url); - console.log('[FreeYT] Content script checking URL:', url, 'hostname:', urlObj.hostname, 'pathname:', urlObj.pathname); - - // Don't process if already converted to no-cookie version - if (urlObj.hostname.includes('yout-ube.com')) { - console.log('[FreeYT] URL already converted to no-cookie version'); - return null; - } - - // Check if it's a video URL - const isVideo = - (urlObj.hostname.includes('youtube.com') && ( - urlObj.pathname.includes('/watch') || // youtube.com/watch?v=xxx - urlObj.pathname.includes('/shorts/') || // youtube.com/shorts/xxx - urlObj.pathname.includes('/embed/') || // youtube.com/embed/xxx - urlObj.pathname.includes('/live/') // youtube.com/live/xxx - )) || - urlObj.hostname === 'youtu.be'; // youtu.be/xxx - - console.log('[FreeYT] Is video URL:', isVideo); - - if (isVideo) { - // Simply replace "youtube" with "yout-ube" in the URL - const converted = url.replace(/youtube/g, 'yout-ube'); - console.log('[FreeYT] Content script converted:', url, '→', converted); - return converted; - } - } catch (e) { - console.error('[FreeYT] Error parsing URL:', e); - } - return null; - } - - // Main redirect logic - async function handleRedirect() { - const enabled = await isEnabled(); - if (!enabled) { - console.log('[FreeYT] Extension disabled, skipping redirect'); - return; - } - - const currentUrl = window.location.href; - const noCookieUrl = convertToNoCookie(currentUrl); - - if (noCookieUrl && noCookieUrl !== currentUrl) { - console.log('[FreeYT] Redirecting to:', noCookieUrl); - window.location.replace(noCookieUrl); - return true; // Indicate we redirected - } - return false; // No redirect needed - } - - // Monitor URL changes for YouTube's dynamic navigation - let lastUrl = window.location.href; - let isRedirecting = false; - - function checkForUrlChange() { - if (isRedirecting) return; // Avoid checking during our own redirect - - const currentUrl = window.location.href; - if (currentUrl !== lastUrl) { - console.log('[FreeYT] URL changed from', lastUrl, 'to', currentUrl); - lastUrl = currentUrl; - - // Small delay to let YouTube finish loading - setTimeout(async () => { - if (!isRedirecting) { - isRedirecting = true; - const didRedirect = await handleRedirect(); - if (!didRedirect) { - isRedirecting = false; - } - } - }, 100); - } - } - - // Listen for popstate events (back/forward navigation) - window.addEventListener('popstate', () => { - console.log('[FreeYT] popstate event detected'); - setTimeout(checkForUrlChange, 50); - }); - - // Listen for pushstate/replacestate (YouTube's navigation) - const originalPushState = history.pushState; - const originalReplaceState = history.replaceState; - - history.pushState = function() { - originalPushState.apply(history, arguments); - console.log('[FreeYT] pushState detected'); - setTimeout(checkForUrlChange, 50); - }; - - history.replaceState = function() { - originalReplaceState.apply(history, arguments); - console.log('[FreeYT] replaceState detected'); - setTimeout(checkForUrlChange, 50); - }; - - // Use MutationObserver to detect when YouTube changes content - const observer = new MutationObserver(() => { - checkForUrlChange(); - }); - - // Start observing changes to the page title and head (YouTube updates these) - if (document.head) { - observer.observe(document.head, { - childList: true, - subtree: true - }); - } - - // Also check periodically in case other methods miss something - setInterval(checkForUrlChange, 2000); - - // Run on page load (initial check) - setTimeout(async () => { - isRedirecting = true; - const didRedirect = await handleRedirect(); - if (!didRedirect) { - isRedirecting = false; - } - }, 100); - - console.log('[FreeYT] Content script initialized with enhanced URL monitoring'); -})(); diff --git a/FreeYT Extension/SafariWebExtensionHandler.swift b/FreeYT Extension/SafariWebExtensionHandler.swift index 5369f16..5905aec 100644 --- a/FreeYT Extension/SafariWebExtensionHandler.swift +++ b/FreeYT Extension/SafariWebExtensionHandler.swift @@ -14,5 +14,5 @@ import Foundation /// No-op placeholder to keep the target compiling. All extension logic lives in -/// manifest.json, rules.json, background.js, and content.js. +/// manifest.json, rules.json, background.js, popup.html/css/js. @objc class SafariWebExtensionHandlerPlaceholder: NSObject {} diff --git a/README.md b/README.md index c9db8d0..f7ca780 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ FreeYT intercepts YouTube links and redirects them to embed-only versions that d - Toggle extension on/off via Safari toolbar popup - Persistent state across browser sessions - Dark/light mode support in popup UI -- Native iOS/Mac host app with modern UIKit interface +- Native iOS/Mac host app with modern SwiftUI interface (liquid glass design) - No data collection, no tracking, no analytics ## Technical Architecture @@ -95,7 +95,9 @@ FreeYT/ ├── FreeYT.xcodeproj # Xcode project file ├── FreeYT/ # Host application │ ├── AppDelegate.swift # App lifecycle management -│ ├── ViewController.swift # Main app screen (native UIKit) +│ ├── SceneDelegate.swift # Scene configuration +│ ├── LiquidGlassView.swift # Main app screen (modern SwiftUI) +│ ├── LiquidGlassHostingController.swift # UIHostingController wrapper │ ├── Info.plist # App bundle configuration │ ├── Assets.xcassets/ # Icons and images │ └── LaunchScreen.storyboard # Splash screen @@ -133,12 +135,13 @@ Safari toolbar popup interface that: - Includes test buttons for verification - Supports system dark/light mode -### ViewController.swift -Native iOS/Mac app UI that: -- Displays app branding and instructions -- Provides button to open Safari settings +### LiquidGlassView.swift +Modern SwiftUI app UI that: +- Displays app branding with liquid glass design +- Shows extension status and instructions +- Supports dark/light mode automatically +- Provides beautiful, polished user experience - Auto-detects extension status on Mac Catalyst -- Shows enable instructions for users ## Bundle Identifiers @@ -207,7 +210,10 @@ Edit the following files: - `FreeYT Extension/Resources/popup.js` - Functionality ### Updating App UI -Edit `FreeYT/ViewController.swift` for host app interface changes. +Edit the following files for host app interface changes: +- `FreeYT/LiquidGlassView.swift` - SwiftUI view structure and layout +- `FreeYT/LiquidGlassHostingController.swift` - UIHostingController wrapper +- `FreeYT/SceneDelegate.swift` - Scene configuration ## Roadmap