diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 8dd648a..fb1f93a 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -14,7 +14,7 @@ if [[ -z "$staged" ]]; then fi if ! command -v xcrun >/dev/null 2>&1; then - red "xcrun not available — install Xcode command-line tools." + red "xcrun not available; install Xcode command-line tools." exit 1 fi diff --git a/.github/workflows/auto-tag.yml b/.github/workflows/auto-tag.yml index 33a9e5e..ff07f75 100644 --- a/.github/workflows/auto-tag.yml +++ b/.github/workflows/auto-tag.yml @@ -9,7 +9,7 @@ name: Auto-tag # # For non-patch bumps (minor / major), use the manual "Bump & Release" # workflow instead. For docs-only / dependabot / hooks-only commits, the -# paths filter below means this workflow doesn't even start — no tag, no +# paths filter below means this workflow doesn't even start, no tag, no # release, no inflation. on: @@ -17,7 +17,7 @@ on: branches: [main] paths: # Only files that affect the produced binary trigger an auto-tag. - # Workflow / CI changes do *not* — that was the source of the + # Workflow / CI changes do *not*, that was the source of the # version-inflation we got while iterating on the workflows themselves. - "Sources/**" - "Tests/**" @@ -43,7 +43,7 @@ jobs: - name: Skip if HEAD already has a v* tag # If the commit is already tagged (e.g. the Bump & Release workflow - # just put one here) there's nothing to do — and re-tagging would + # just put one here) there's nothing to do, and re-tagging would # collide. Sets `skip=true` for downstream conditions. id: skip run: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c0b85c..64e1d67 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: # Only fires the actual job when a PR carries the `run-ci` label, or when - # someone manually clicks "Run workflow" — keeps macOS minutes from being + # someone manually clicks "Run workflow", keeps macOS minutes from being # burned on every push. pull_request: types: [opened, synchronize, reopened, labeled] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2365946..6ef46fe 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,7 @@ # Contributing to YouMenuTube Thanks for considering a contribution! YouMenuTube is a small SwiftUI -menu-bar app and the codebase is intentionally compact — read the README +menu-bar app and the codebase is intentionally compact; read the README first to get a feel for what the app does and the constraints it operates under (it's an unofficial client wrapping YouTube's internal **InnerTube** API; see the disclaimer in the README). @@ -21,15 +21,15 @@ Then ⌘R in Xcode. Requires **macOS 15 Sequoia** and **Xcode 16+**. See the "Project layout" section of the [README](README.md). High-level: -- `Sources/App/` — `@main` entry, scenes, root view. -- `Sources/Services/` — `YouTubeService` (the only network surface), +- `Sources/App/`: `@main` entry, scenes, root view. +- `Sources/Services/`: `YouTubeService` (the only network surface), `PlayerController`, `RefreshTrigger`, `Keychain`. -- `Sources/Views/` — one SwiftUI view per tab, plus the player and sign-in +- `Sources/Views/`: one SwiftUI view per tab, plus the player and sign-in windows. `VideoFeedList` / `VideoList` / `ErrorInline` are the shared building blocks. -- `Sources/Models/` — `VideoEntry`, `PlaylistEntry`. -- `Sources/Utilities/` — `WindowID`, `UserAgent`, `BuiltInPlaylist`. -- `Tests/` — Swift Testing target. +- `Sources/Models/`: `VideoEntry`, `PlaylistEntry`. +- `Sources/Utilities/`: `WindowID`, `UserAgent`, `BuiltInPlaylist`. +- `Tests/`: Swift Testing target. ## Format, lint, test @@ -64,7 +64,7 @@ Configuration lives in [`.swift-format`](.swift-format) (line length 120, ## CI -GitHub Actions on every PR — but the actual `build-test` job runs **only** +GitHub Actions run on every PR, but the actual `build-test` job runs **only** when the PR carries the `run-ci` label, or when triggered manually from the Actions tab. This keeps macOS minutes from being burned on every push. A maintainer will usually add the label after a quick eyeball. @@ -75,11 +75,11 @@ maintainer will usually add the label after a quick eyeball. - Match the existing style: terse comments, no hypothetical generality, no dead code, identifiers should carry meaning instead of comments explaining what code does. (See [`CLAUDE.md` in the system prompt of the maintainers' - AI tooling](.) — same principles apply to humans.) + AI tooling](.); same principles apply to humans.) - Don't add code-level documentation that just narrates the next line. Do add a short comment when there's a non-obvious *why* (a workaround, a hidden constraint, a quirk of YouTubeKit / Google fingerprinting, etc.). -- Run `swift-format format -i -r ...` before committing — the pre-commit +- Run `swift-format format -i -r ...` before committing; the pre-commit hook will reject unformatted files. - Open an issue first for anything that touches the auth flow, the InnerTube cookie filter, the browser cookie readers, or the player window's WKWebView @@ -92,7 +92,7 @@ maintainer will usually add the label after a quick eyeball. - Imperative mood: "Add Home tab", not "Added" or "Adds". - Subject ≤ 72 characters; explain *why* in the body, not *what*. -- One topic per commit. We don't squash on merge — keep history readable. +- One topic per commit. We don't squash on merge; keep history readable. ## Reporting bugs @@ -106,7 +106,7 @@ soon). For now include: - Relevant log output: `log stream --predicate 'subsystem == "app.youmenutube"' --level debug` -For **security** issues, see [`SECURITY.md`](SECURITY.md) — please don't +For **security** issues, see [`SECURITY.md`](SECURITY.md); please don't file them publicly. ## License diff --git a/NOTICE b/NOTICE index 24292f0..3a1a7d4 100644 --- a/NOTICE +++ b/NOTICE @@ -5,5 +5,5 @@ Licensed under the Apache License, Version 2.0. See LICENSE for the full text. This product bundles the following third-party software: - YouTubeKit (https://github.com/b5i/YouTubeKit) — MIT License + YouTubeKit (https://github.com/b5i/YouTubeKit): MIT License Copyright 2023 - 2025 Antoine Bollengier diff --git a/README.md b/README.md index d40f1e4..17f8e34 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Website: A macOS menu-bar app that puts YouTube in your menubar: -- **Home** (default tab) — YouTube's main recommendations feed +- **Home** (default tab): YouTube's main recommendations feed - Latest videos from your **Subscriptions** - Browse your **Playlists** (including **Watch Later** and **Liked Videos**) - **Search** YouTube @@ -20,14 +20,14 @@ A macOS menu-bar app that puts YouTube in your menubar: - In-app **update check** against the latest GitHub release Built with SwiftUI `MenuBarExtra`, targeting **macOS 15 Sequoia or later**. -Powered by [YouTubeKit](https://github.com/b5i/YouTubeKit) — talks to +Powered by [YouTubeKit](https://github.com/b5i/YouTubeKit). It talks to YouTube's own internal "InnerTube" API directly. **No Google Cloud project, no API key, no OAuth client to set up.** Sign into youtube.com in your -usual browser — passkeys, password managers, all of it — and YouMenuTube +usual browser (passkeys, password managers, all of it) and YouMenuTube imports the session. Supports Safari, Chrome, Edge, Arc, Brave, Vivaldi, Opera, Helium, Firefox and Zen. -> ## ⚠️ Important — read before installing +> ## ⚠️ Important: read before installing > > **YouMenuTube is unofficial and not affiliated with, endorsed by, or > sponsored by YouTube, Google, or Alphabet.** All trademarks belong to @@ -38,7 +38,7 @@ Opera, Helium, Firefox and Zen. > YouTube's internal **InnerTube** API via [YouTubeKit](https://github.com/b5i/YouTubeKit). > This is the same surface YouTube's own website uses, but it is **not a > public API** and using it is **arguably outside YouTube's Terms of -> Service**. The API can — and occasionally does — change or break without +> Service**. The API can (and occasionally does) change or break without > notice. > > **Use at your own risk.** Recommendations: @@ -55,7 +55,7 @@ Opera, Helium, Firefox and Zen. (that link permanently redirects to the newest published version, so you can bookmark it). 2. Open the DMG and drag `YouMenuTube.app` into `/Applications`. -3. **First launch** — because the build is ad-hoc signed (no paid Apple +3. **First launch**: because the build is ad-hoc signed (no paid Apple Developer ID), Gatekeeper will refuse it on a normal double-click. Either: - Right-click the app → **Open** → **Open** again in the prompt, *or* - System Settings → Privacy & Security → "YouMenuTube was blocked …" → @@ -129,17 +129,17 @@ YouMenuTube/ ## Setup ```sh -brew install xcodegen # (if missing — bootstrap.sh will offer to do this for you) +brew install xcodegen # (if missing, bootstrap.sh will offer to install it for you) ./bootstrap.sh ``` This generates `YouMenuTube.xcodeproj`, resolves `YouTubeKit` via SPM, and opens Xcode. In Xcode: -1. Pick a signing team (target → **Signing & Capabilities → Team**) — a free +1. Pick a signing team (target → **Signing & Capabilities → Team**); a free personal team works. 2. Build & Run (⌘R). -3. There's no Dock icon (by design — `LSUIElement`). Look for the ▶️ icon in +3. There's no Dock icon (by design: `LSUIElement`). Look for the ▶️ icon in the menu bar. 4. Click the icon → **Sign in** → pick the browser where you're already signed in to youtube.com. First import may ask for a one-time permission @@ -153,7 +153,7 @@ opens Xcode. In Xcode: The project ships a second scheme, **YouMenuTube Demo**, which launches the app with `-demo-mode`. In that mode `YouTubeService` swaps every network call for the hardcoded fixtures in -[`Sources/Services/DemoData.swift`](Sources/Services/DemoData.swift) — +[`Sources/Services/DemoData.swift`](Sources/Services/DemoData.swift): popular real video IDs, so thumbnails render normally and clicking a row plays the real video in the embedded player. No sign-in, no keychain, no InnerTube traffic. Edit `DemoData.swift` to change what the screenshots @@ -184,7 +184,7 @@ YMT_DEMO_MODE=1 ./path/to/YouMenuTube.app/Contents/MacOS/YouMenuTube YouMenuTube doesn't host a sign-in UI of its own. Instead it imports `youtube.com` cookies directly out of your browser's cookie store, so the sign-in itself happens wherever your passkeys / password manager already -work — your normal browser. Three formats covered: +work: in your normal browser. Three formats covered: | Browser family | Storage | What macOS will ask | |---|---|---| @@ -192,7 +192,7 @@ work — your normal browser. Three formats covered: | **Chrome / Edge / Arc / Brave / Vivaldi / Opera / Helium** (Chromium family) | SQLite + AES-128-CBC with a key in the login Keychain | A standard "YouMenuTube wants to use confidential information stored in 'Chrome Safe Storage' …" Keychain prompt. Click **Always Allow** once per browser. (Helium's Keychain entry is `Helium Storage Key` rather than the usual `… Safe Storage`, but the flow is identical.) | | **Firefox / Zen** (Firefox family) | Plain SQLite, unencrypted | No prompt. | -The importer filters strictly to `*.youtube.com`-scoped rows — mixing in +The importer filters strictly to `*.youtube.com`-scoped rows; mixing in `.google.com` or `accounts.google.com` cookies makes InnerTube respond with `loggedOut=true`. The resulting blob is persisted in the macOS Keychain and handed to YouTubeKit via `YouTubeModel.cookies`. @@ -204,14 +204,14 @@ once signed in and click **Import**. The Now Playing window wraps `youtube.com/embed/` in a tiny HTML page loaded with `baseURL = https://youmenutube.local/`. The fake-but-real-looking -parent origin is what makes YouTube's IFrame player initialize — loading +parent origin is what makes YouTube's IFrame player initialize. Loading the embed URL top-level returns error 153, and `baseURL = youtube.com` (same-origin parent) returns 152-4. The window itself uses `.windowStyle(.plain)` for a fully chrome-less look, locked to a 16:9 aspect ratio via `NSWindow.aspectRatio`. Because plain (borderless) windows can't normally become key or be resized, the underlying -`NSWindow`'s styleMask is patched to add `.resizable` — which restores both +`NSWindow`'s styleMask is patched to add `.resizable`, which restores both edge-resize handles and Cmd+W. A 28pt strip at the top of the player fades in on hover, hosting a small ✕ close button and a `performDrag(with:)`-backed draggable region (since the WKWebView itself swallows mouse events). By @@ -234,19 +234,19 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for PR conventions and detail. Releases are fully automated. The pipeline: -1. **Auto-tag** (`.github/workflows/auto-tag.yml`) — every push to `main` that +1. **Auto-tag** (`.github/workflows/auto-tag.yml`): every push to `main` that touches `Sources/`, `Tests/`, `project.yml`, or `bootstrap.sh` gets a patch bump (e.g. `v0.1.5 → v0.1.6`). Docs / dependabot / hook / format-config pushes don't trigger it. -2. **Release** (`.github/workflows/release.yml`) — fires on `v*` tag pushes, +2. **Release** (`.github/workflows/release.yml`): fires on `v*` tag pushes, builds the `.app` (Release config), ad-hoc signs it, packages it as a `.dmg` with `create-dmg`, and publishes a GitHub Release. -3. **Bump & Release** (`.github/workflows/bump-release.yml`) — manual entry +3. **Bump & Release** (`.github/workflows/bump-release.yml`): manual entry point (Actions tab → Run workflow) for `minor` / `major` bumps. Patches are handled by Auto-tag. Versioning: `CFBundleShortVersionString` is derived from `git describe` -(post-build script in `project.yml`) — `0.1.0` on a tag, `0.1.0+N` past it. +(post-build script in `project.yml`): `0.1.0` on a tag, `0.1.0+N` past it. `CFBundleVersion` is `git rev-list --count HEAD` (monotonic, kept for macOS update bookkeeping even though it isn't shown in UI). `GitCommit` holds the short SHA (with a `-dirty` suffix when the working tree has @@ -255,28 +255,28 @@ commit, e.g. `0.1.0 · 19d5410`. ## Troubleshooting -- **Import says "Safari's cookies live inside a protected container"** — +- **Import says "Safari's cookies live inside a protected container"**: macOS requires **Full Disk Access** to read `~/Library/Containers/com.apple.Safari/…`. System Settings → Privacy & Security → Full Disk Access → add YouMenuTube (the Import window has a shortcut button for this). Re-run Import. - **Import says "Couldn't read [Browser]'s cookie-encryption key from the - Keychain"** — you clicked Deny on the macOS Keychain prompt. Open + Keychain"**: you clicked Deny on the macOS Keychain prompt. Open Keychain Access, search for "Safe Storage" for that browser, open the entry → **Access Control** tab → remove YouMenuTube from the deny list (or delete the ACL), then re-run Import and click **Always Allow**. -- **Import says "[Browser] isn't signed in to YouTube"** — the browser +- **Import says "[Browser] isn't signed in to YouTube"**: the browser you picked doesn't have a valid YouTube session. Click the "Sign in to YouTube in [Browser]" button, complete sign-in there, then re-run Import. -- **Subscriptions / playlists empty after signing in** — enable verbose +- **Subscriptions / playlists empty after signing in**: enable verbose logging to see what the import produced: `log stream --predicate 'subsystem == "app.youmenutube"' --level debug`. If session markers (SAPISID, SID, LOGIN_INFO, __Secure-3PSIDTS) are all present but InnerTube still says `loggedOut=true`, YouTubeKit may be out - of date — check for a newer release. -- **Embed player shows error 150 / 101** — the uploader has disabled + of date; check for a newer release. +- **Embed player shows error 150 / 101**: the uploader has disabled embedding for that specific video; it can only be played on youtube.com. -- **Embed player shows error 153 / 152-4** — should not happen with the +- **Embed player shows error 153 / 152-4**: should not happen with the current wrapper; if it does, check `Sources/Views/PlayerWindow.swift` for changes to `baseURL` or the iframe `origin` query parameter. @@ -290,7 +290,7 @@ network only for: - `api.github.com/repos/gek0z/YouMenuTube/releases/latest` once per launch to render the update-available link in Settings → About -Sign-in itself happens in your browser, not in YouMenuTube — the app +Sign-in itself happens in your browser, not in YouMenuTube; the app doesn't make network requests to Google / YouTube for authentication. It reads `youtube.com` cookies out of your browser's on-disk cookie store (filtered to `*.youtube.com` only), stores them in the macOS Keychain diff --git a/SECURITY.md b/SECURITY.md index 8d7f9ad..05ab76f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -13,7 +13,7 @@ Please **do not** open a public GitHub issue for security problems. Use one of the following private channels: - GitHub's [private vulnerability reporting](https://github.com/gek0z/YouMenuTube/security/advisories/new) - (preferred — gives us a private discussion thread + draft advisory). + (preferred: gives us a private discussion thread + draft advisory). - Email the maintainer (see the GitHub profile of [@gek0z](https://github.com/gek0z)). Please include: @@ -25,12 +25,12 @@ Please include: 4. Whether you'd like to be credited in the advisory. We aim to acknowledge reports within 7 days. Because this is a hobby project, -fix turnaround depends on availability — critical issues will be prioritised. +fix turnaround depends on availability; critical issues will be prioritised. ## Supported versions Only `main` is supported. Older tagged releases will not receive backported -fixes — upgrade to the latest release. +fixes; upgrade to the latest release. ## Scope @@ -43,7 +43,7 @@ In scope: Out of scope: - Vulnerabilities in YouTube, Google, or any first-party Apple framework. -- Vulnerabilities in third-party dependencies (report those upstream — currently +- Vulnerabilities in third-party dependencies (report those upstream; currently only [YouTubeKit](https://github.com/b5i/YouTubeKit)). - The fact that YouMenuTube uses YouTube's internal **InnerTube** API. This is documented in the README and is a deliberate design choice, not a security @@ -67,7 +67,7 @@ of the selected browser's on-disk cookie store: - **Safari** requires the user to grant YouMenuTube **Full Disk Access** (System Settings → Privacy & Security → Full Disk Access) because the Safari cookie file lives inside a protected container. Without this grant - the reader fails cleanly — it cannot silently read Safari cookies. + the reader fails cleanly; it cannot silently read Safari cookies. - **Chromium-family browsers** (Chrome, Edge, Arc, Brave, Vivaldi, Opera, Helium) encrypt their cookie values with an AES key stored in the macOS login Keychain under e.g. `Chrome Safe Storage` (or `Helium Storage Key` @@ -89,4 +89,4 @@ present. No other domain's cookies are ever persisted or sent. InnerTube endpoints. - No background activity when the menu bar popover is closed. -If you find evidence to the contrary, that's a bug — please report it. +If you find evidence to the contrary, that's a bug; please report it. diff --git a/Sources/Services/BrowserCookieImport/Browser.swift b/Sources/Services/BrowserCookieImport/Browser.swift index ef4c689..69f205d 100644 --- a/Sources/Services/BrowserCookieImport/Browser.swift +++ b/Sources/Services/BrowserCookieImport/Browser.swift @@ -94,7 +94,7 @@ enum Browser: String, CaseIterable, Identifiable { /// as a `kSecClassGenericPassword` entry in the login keychain. /// /// Most Chromium forks use " Safe Storage" with account "", - /// but Helium uses "Helium Storage Key" / "Helium" — so this is handled + /// but Helium uses "Helium Storage Key" / "Helium", so this is handled /// per-case rather than synthesised from `displayName`. var chromiumSafeStorageService: String? { switch self { diff --git a/Sources/Services/BrowserCookieImport/BrowserDetector.swift b/Sources/Services/BrowserCookieImport/BrowserDetector.swift index 0acebea..1900f43 100644 --- a/Sources/Services/BrowserCookieImport/BrowserDetector.swift +++ b/Sources/Services/BrowserCookieImport/BrowserDetector.swift @@ -2,7 +2,7 @@ import Foundation /// Finds which browsers the user actually has a cookie store for on this /// machine, so the import UI only offers real choices. We don't use -/// `NSWorkspace.urlForApplication(withBundleIdentifier:)` — an app being +/// `NSWorkspace.urlForApplication(withBundleIdentifier:)`, an app being /// installed doesn't mean it has ever been run, and without a cookie store /// there's nothing to import. Presence of the data directory is the real /// signal. @@ -16,7 +16,7 @@ enum BrowserDetector { // We *can't* stat the Safari container without Full Disk Access // (the stat itself is gated by TCC on macOS 13+). Always list // Safari and let the reader surface the TCC error if FDA hasn't - // been granted — that's a clearer UX than silently hiding it. + // been granted, that's a clearer UX than silently hiding it. return true case .chromium: guard let root = browser.userDataRoot(home: home) else { return false } diff --git a/Sources/Services/BrowserCookieImport/ChromiumCookies.swift b/Sources/Services/BrowserCookieImport/ChromiumCookies.swift index 17ae9ec..a2d7732 100644 --- a/Sources/Services/BrowserCookieImport/ChromiumCookies.swift +++ b/Sources/Services/BrowserCookieImport/ChromiumCookies.swift @@ -61,7 +61,7 @@ enum ChromiumCookies { if let decrypted = Self.decrypt(data, key: key, hostKey: host) { value = decrypted } else { - // Value was encrypted but we couldn't decrypt it — + // Value was encrypted but we couldn't decrypt it, // skip rather than ship a corrupt cookie. continue } @@ -89,7 +89,7 @@ enum ChromiumCookies { private static func fetchSafeStoragePassword(service: String, account: String, browser: Browser) throws -> String { // Passing an LAContext lets the system upgrade the prompt to Touch - // ID on Macs that have it enrolled — but only if the keychain + // ID on Macs that have it enrolled, but only if the keychain // item's ACL was created with biometric-compatible flags. Chromium // browsers don't do that when they add their Safe Storage key, so // in practice the OS still falls back to a password prompt. Harmless diff --git a/Sources/Services/BrowserCookieImport/FirefoxCookies.swift b/Sources/Services/BrowserCookieImport/FirefoxCookies.swift index b7895df..a5761d2 100644 --- a/Sources/Services/BrowserCookieImport/FirefoxCookies.swift +++ b/Sources/Services/BrowserCookieImport/FirefoxCookies.swift @@ -2,7 +2,7 @@ import Foundation import SQLite3 /// Reads cookies out of Firefox's `cookies.sqlite`. Firefox stores cookie -/// values in plaintext, so no key derivation or decryption is needed — just +/// values in plaintext, so no key derivation or decryption is needed, just /// SQLite. /// /// The file can be opened even while Firefox is running because we open it diff --git a/Sources/Services/BrowserCookieImport/SafariBinaryCookies.swift b/Sources/Services/BrowserCookieImport/SafariBinaryCookies.swift index e378fd7..9a6f5ce 100644 --- a/Sources/Services/BrowserCookieImport/SafariBinaryCookies.swift +++ b/Sources/Services/BrowserCookieImport/SafariBinaryCookies.swift @@ -14,7 +14,7 @@ import Foundation /// 4B BE number of pages N /// N×4B BE page sizes /// pages concatenated -/// [checksum / footer — ignored] +/// [checksum / footer, ignored] /// /// Page (little-endian after the 4-byte BE page tag): /// 4B 0x00000100 page tag @@ -33,7 +33,7 @@ import Foundation /// 4B path offset /// 4B value offset /// 8B unused -/// 8B expiry (CFAbsoluteTime — seconds since 2001-01-01 UTC) +/// 8B expiry (CFAbsoluteTime, seconds since 2001-01-01 UTC) /// 8B creation (CFAbsoluteTime) /// NUL-terminated strings at the declared offsets enum SafariBinaryCookies { @@ -52,7 +52,7 @@ enum SafariBinaryCookies { } // macOS returns "operation not permitted" for TCC-blocked reads, // which Foundation surfaces as generic; treat any read failure - // on a path that exists as TCC-denied — user can retry after + // on a path that exists as TCC-denied, user can retry after // granting Full Disk Access. throw BrowserCookieError.tccDenied } @@ -90,7 +90,7 @@ enum SafariBinaryCookies { private static func parsePage(_ page: Data, into cookies: inout [HTTPCookie], domainSuffix: String) throws { guard page.count >= 12 else { return } - // Page tag is 4 bytes but we don't validate it strictly — some + // Page tag is 4 bytes but we don't validate it strictly, some // Safari builds have varied the first byte. let cookieCount = Int(page.readUInt32LE(at: 4)) guard cookieCount > 0, page.count >= 8 + cookieCount * 4 + 4 else { return } @@ -127,7 +127,7 @@ enum SafariBinaryCookies { let value = rec.readCString(at: valueOff) else { return nil } - // Early domain filter — 99% of cookies won't match, and this avoids + // Early domain filter, 99% of cookies won't match, and this avoids // building HTTPCookie objects we'd drop anyway. guard domain.hasSuffix(domainSuffix) else { return nil } diff --git a/Sources/Services/DemoData.swift b/Sources/Services/DemoData.swift index 903a545..f6a3a8f 100644 --- a/Sources/Services/DemoData.swift +++ b/Sources/Services/DemoData.swift @@ -4,7 +4,7 @@ import Foundation /// is launched in demo mode (`-demo-mode` / `YMT_DEMO_MODE=1`). Populated /// with real, famous YouTube video IDs so thumbnails render from /// `i.ytimg.com` and clicking a row still plays the real video in the -/// embedded player — no network call needed to produce the row itself. +/// embedded player, no network call needed to produce the row itself. /// /// To change the look of the screenshots, edit the arrays below. enum DemoData { @@ -12,7 +12,7 @@ enum DemoData { // MARK: - Videos /// Builds a `VideoEntry` using `https://i.ytimg.com/vi//hqdefault.jpg` - /// for the thumbnail — which reliably exists for any real video ID. + /// for the thumbnail, which reliably exists for any real video ID. private static func video( _ id: String, _ title: String, @@ -33,7 +33,7 @@ enum DemoData { ) } - /// What the Home tab shows in demo mode. Classic memes / viral videos — + /// What the Home tab shows in demo mode. Classic memes / viral videos, /// thumbnails all come from `i.ytimg.com` for these real IDs. Order is /// curated so the most-recently-added entries land above the fold of /// the 420×560 popover (~6 rows visible without scrolling). @@ -47,7 +47,7 @@ enum DemoData { channel: "Protectstar Inc.", time: "15 years ago", views: "14M views", duration: "1:20:16"), video( - "92fLApYaCGI", "Michael Jordan \"The Last Shot\" – #NBATogetherLive Classic Game", + "92fLApYaCGI", "Michael Jordan \"The Last Shot\", #NBATogetherLive Classic Game", channel: "NBA", time: "6 years ago", views: "32M views", duration: "2:33"), video( @@ -178,7 +178,7 @@ enum DemoData { } } - /// Seed set for the "is this in my Watch Later?" cache — drives the + /// Seed set for the "is this in my Watch Later?" cache, drives the /// clock badge state on Home rows on first launch. static let watchLaterIds: Set = ["dQw4w9WgXcQ", "tzD9OxAHtzU", "XnygT6ANLzQ", "dMH0bHeiRNg"] @@ -186,8 +186,8 @@ enum DemoData { /// Very cheap substring match so typing in the search box in demo mode /// actually narrows a visible list. Falls back to everything when the - /// query is empty (shouldn't happen — the view short-circuits empty - /// queries — but harmless). + /// query is empty (shouldn't happen, the view short-circuits empty + /// queries, but harmless). static func search(_ query: String) -> [VideoEntry] { let q = query.lowercased().trimmingCharacters(in: .whitespaces) guard !q.isEmpty else { return homeFeed } diff --git a/Sources/Services/DockPresence.swift b/Sources/Services/DockPresence.swift index de776d9..c041f16 100644 --- a/Sources/Services/DockPresence.swift +++ b/Sources/Services/DockPresence.swift @@ -4,7 +4,7 @@ import Observation /// Tracks which "dockable" windows (player, sign-in) are currently on /// screen and flips `NSApp`'s activation policy accordingly. The app /// launches as `.accessory` (menubar-only, LSUIElement=true) and -/// switches to `.regular` while any real window is visible — giving the +/// switches to `.regular` while any real window is visible, giving the /// user a Dock icon to switch to, a standard application menu, and a /// Cmd+Tab slot. /// diff --git a/Sources/Services/PlayerController.swift b/Sources/Services/PlayerController.swift index 2451238..b2bc4c7 100644 --- a/Sources/Services/PlayerController.swift +++ b/Sources/Services/PlayerController.swift @@ -12,7 +12,7 @@ final class PlayerController { self.videoId = videoId self.title = title Self.closeMenuBarPopover() - // Activation happens in PlayerWindow.onAppear / onChange — calling + // Activation happens in PlayerWindow.onAppear / onChange, calling // NSApp.activate here is a no-op while the app is still .accessory // (the policy flip only lands once the window has appeared). } diff --git a/Sources/Services/UpdateChecker.swift b/Sources/Services/UpdateChecker.swift index 35ac6dd..f3f1105 100644 --- a/Sources/Services/UpdateChecker.swift +++ b/Sources/Services/UpdateChecker.swift @@ -31,7 +31,7 @@ final class UpdateChecker { self.session = session } - /// Bundle's `CFBundleShortVersionString` — the value the comparison is + /// Bundle's `CFBundleShortVersionString`, the value the comparison is /// made against. Anything non-semver (e.g. "main-abc1234" from a nightly) /// is treated as "older than any tagged release", which is the right /// behaviour for users on the rolling channel. diff --git a/Sources/Services/YouTubeService.swift b/Sources/Services/YouTubeService.swift index c7c26d9..c79015d 100644 --- a/Sources/Services/YouTubeService.swift +++ b/Sources/Services/YouTubeService.swift @@ -7,8 +7,8 @@ private let log = Logger(subsystem: "app.youmenutube", category: "yt-service") /// Single auth + API surface for the app. Uses YouTubeKit (InnerTube) for /// everything, signed in via cookies imported from the user's own browser -/// (Safari / Chrome / Firefox / etc — see `BrowserCookieImporter`). No -/// Google Cloud project, no API key, no OAuth — just a YouTube login +/// (Safari / Chrome / Firefox / etc.; see `BrowserCookieImporter`). No +/// Google Cloud project, no API key, no OAuth; just a YouTube login /// handed to us by the browser the user already uses. /// /// Trade-off: InnerTube is YouTube's *internal* API. It can break and is @@ -20,7 +20,7 @@ final class YouTubeService { var lastError: String? /// Video IDs known to be in the user's Watch Later. Populated lazily on - /// sign-in (first page only — covers the most-recent items) and updated + /// sign-in (first page only, covers the most-recent items) and updated /// optimistically on add/remove. Reads are local; only the toggle action /// itself hits the network. private(set) var watchLaterIds: Set = [] @@ -37,7 +37,7 @@ final class YouTubeService { // Pretend we're signed in with a pre-seeded Watch Later so every // tab has content and the "Sign in" chrome is hidden. No keychain // read, no network. - log.notice("demo mode active — serving fixtures from DemoData.swift") + log.notice("demo mode active, serving fixtures from DemoData.swift") isSignedIn = true watchLaterIds = DemoData.watchLaterIds return @@ -76,13 +76,13 @@ final class YouTubeService { lastError = nil } - /// Called when any authenticated endpoint reports `isDisconnected` — + /// Called when any authenticated endpoint reports `isDisconnected`, /// YouTube rejected our cookies despite them being present locally. /// Wipes the stale session so every tab (Home included) reflects the /// signed-out state consistently, instead of Home silently falling /// through to the public feed while other tabs surface an error. private func handleDisconnected() { - log.notice("server reported disconnected session — wiping local cookies") + log.notice("server reported disconnected session, wiping local cookies") lastError = YTServiceError.disconnected.errorDescription signOut() } @@ -101,7 +101,7 @@ final class YouTubeService { guard !relevant.isEmpty else { return false } // A browser stores cookies keyed by (name, domain, path) and sends - // only the ones whose path prefixes the request path — one value + // only the ones whose path prefixes the request path, one value // per name, with the longest matching path winning. If we concatenate // every row from the store we can end up with e.g. three // `VISITOR_INFO1_LIVE=…` values in the header (paths `/`, `/embed`, @@ -250,7 +250,7 @@ final class YouTubeService { } } - /// Reload the Watch Later membership cache. Only fetches the first page — + /// Reload the Watch Later membership cache. Only fetches the first page, /// far-back items won't show as "saved" until the user toggles them, but /// the optimistic local cache keeps recent toggles accurate. func refreshWatchLaterIds() { @@ -326,7 +326,7 @@ enum YTServiceError: LocalizedError { case .notSignedIn: return "Sign in to YouTube first." case .disconnected: return - "YouTube rejected the session. Sign out and re-import from your browser — make sure the browser is signed in to youtube.com, not just accounts.google.com." + "YouTube rejected the session. Sign out and re-import from your browser. Make sure the browser is signed in to youtube.com, not just accounts.google.com." case .actionFailed: return "YouTube rejected the action." } diff --git a/Sources/Utilities/Collections.swift b/Sources/Utilities/Collections.swift index 24c8d81..9bd354c 100644 --- a/Sources/Utilities/Collections.swift +++ b/Sources/Utilities/Collections.swift @@ -3,7 +3,7 @@ import Foundation extension Array { /// Returns elements with unique values at `keyPath`, keeping the first /// occurrence. Used to normalise model arrays before handing them to - /// `ForEach` — YouTubeKit occasionally returns the same video id twice + /// `ForEach`, YouTubeKit occasionally returns the same video id twice /// in one response (e.g. a shelf item that also appears inline), which /// triggers SwiftUI runtime warnings about duplicate ids. func uniqued(by keyPath: KeyPath) -> [Element] { diff --git a/Sources/Views/ImportSessionWindow.swift b/Sources/Views/ImportSessionWindow.swift index a5aa098..2f9236e 100644 --- a/Sources/Views/ImportSessionWindow.swift +++ b/Sources/Views/ImportSessionWindow.swift @@ -5,8 +5,8 @@ import SwiftUI private let importLog = Logger(subsystem: "app.youmenutube", category: "import-session") /// Replacement for the old `YouTubeSignInWindow`. The user signs in to -/// YouTube *in their own browser* — where passkeys and password managers -/// all work natively — and we import their `youtube.com` cookies from that +/// YouTube *in their own browser*, where passkeys and password managers +/// all work natively, and we import their `youtube.com` cookies from that /// browser's on-disk cookie store. /// /// See `BrowserCookieImporter` for the mechanics, and issue #8 for why the @@ -56,7 +56,7 @@ struct ImportSessionWindow: View { Text("Import your YouTube session") .font(.title2).bold() Text( - "Sign in to YouTube in your browser — passkeys, password managers, and all. Then pick that browser below and we'll copy the session into YouMenuTube." + "Sign in to YouTube in your browser (passkeys, password managers, and all). Then pick that browser below and we'll copy the session into YouMenuTube." ) .font(.callout) .foregroundStyle(.secondary) @@ -117,7 +117,7 @@ struct ImportSessionWindow: View { Banner( icon: "lock.shield", text: - "macOS will ask for your login password once — that's the standard Keychain prompt that lets YouMenuTube read \(browser.displayName)'s cookie-encryption key. Click **Always Allow** to skip it on later imports. Touch ID may appear on Macs that support it." + "macOS will ask for your login password once. That's the standard Keychain prompt that lets YouMenuTube read \(browser.displayName)'s cookie-encryption key. Click **Always Allow** to skip it on later imports. Touch ID may appear on Macs that support it." ) case .safari: Banner( @@ -241,14 +241,14 @@ struct ImportSessionWindow: View { } private func bringToFront() { - // `NSApp.activate(ignoringOtherApps:)` was softened in macOS 14 — it + // `NSApp.activate(ignoringOtherApps:)` was softened in macOS 14, it // only activates if the caller was recently user-facing, which a // MenuBarExtra popover is not once it closes. `activate()` (no arg) // is the replacement and works reliably for LSUIElement apps. NSApp.activate() // The NSWindow usually isn't in `NSApp.windows` yet when `onAppear` // fires on first open. Poll a few times instead of assuming one - // runloop is enough — fixes the "Sign in button did nothing" + // runloop is enough, fixes the "Sign in button did nothing" // symptom when the host app is an LSUIElement + MenuBarExtra. Task { @MainActor in for _ in 0..<10 { diff --git a/Sources/Views/PlayerWindow.swift b/Sources/Views/PlayerWindow.swift index e19b3ac..898ea58 100644 --- a/Sources/Views/PlayerWindow.swift +++ b/Sources/Views/PlayerWindow.swift @@ -53,7 +53,7 @@ struct PlayerWindow: View { } .onChange(of: player.videoId) { _, newId in // Playing a fresh video while the window is already open - // should still pull focus — otherwise the menubar click + // should still pull focus, otherwise the menubar click // swaps the video silently in the background. if newId != nil { bringToFront() } } diff --git a/Sources/Views/SettingsView.swift b/Sources/Views/SettingsView.swift index d93a55d..61428dd 100644 --- a/Sources/Views/SettingsView.swift +++ b/Sources/Views/SettingsView.swift @@ -37,7 +37,7 @@ struct SettingsView: View { } } Text( - "Uses YouTube's own internal API via your browser session — no Google Cloud setup needed. Personal use only." + "Uses YouTube's own internal API via your browser session. Personal use only." ) .font(.caption2).foregroundStyle(.secondary) } @@ -133,7 +133,7 @@ struct SettingsView: View { if let commit { return "\(head) · \(commit)" } - return "\(head) — \(build)" + return "\(head) · \(build)" } @ViewBuilder diff --git a/Sources/Views/VideoRow.swift b/Sources/Views/VideoRow.swift index fac5739..d3c5774 100644 --- a/Sources/Views/VideoRow.swift +++ b/Sources/Views/VideoRow.swift @@ -113,7 +113,7 @@ struct ThumbnailView: View { Color.gray.opacity(0.15) } } - // Disable the implicit cross-fade — its layout pass jitters LazyVStack + // Disable the implicit cross-fade, its layout pass jitters LazyVStack // when many rows finish loading in quick succession during fast scrolls. .transaction { $0.animation = nil } } diff --git a/bootstrap.sh b/bootstrap.sh index 09aee42..f884a23 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # ───────────────────────────────────────────────────────────────────────────── -# YouMenuTube — one-shot bootstrap +# YouMenuTube: one-shot bootstrap # # Usage: # ./bootstrap.sh # install prereqs, generate project, open Xcode @@ -45,7 +45,7 @@ xcode_ver=$(awk 'NR==1 { print $2 }' <<<"$xcodebuild_out") xcode_ver=${xcode_ver:-0} xcode_major=${xcode_ver%%.*} if ! [[ "$xcode_major" =~ ^[0-9]+$ ]] || (( xcode_major < 16 )); then - yellow " Xcode $xcode_ver detected — this project targets macOS 15 and expects Xcode 16+." + yellow " Xcode $xcode_ver detected; this project targets macOS 15 and expects Xcode 16+." fi green " ✓ macOS + Xcode $xcode_ver" diff --git a/project.yml b/project.yml index ece8f32..6475c82 100644 --- a/project.yml +++ b/project.yml @@ -103,7 +103,7 @@ targets: AHEAD=$(echo "$DESC" | sed -nE 's/.*-([0-9]+)-g[0-9a-f]+$/\1/p') IS_DEV="dev+${AHEAD}" else - SHORT="" # no tags — keep whatever project.yml put in there + SHORT="" # no tags, keep whatever project.yml put in there IS_DEV="dev" fi [ -n "$SHORT" ] && /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $SHORT" "$INFO_PLIST" @@ -136,7 +136,7 @@ targets: schemes: YouMenuTube: - # Explicit so it coexists with the Demo scheme below — defining any + # Explicit so it coexists with the Demo scheme below, defining any # top-level schemes disables xcodegen's autogeneration for the target. build: targets: diff --git a/tools/gen_app_icon.swift b/tools/gen_app_icon.swift index 6bf807a..720a740 100644 --- a/tools/gen_app_icon.swift +++ b/tools/gen_app_icon.swift @@ -35,7 +35,7 @@ func render(sizePx: Int) -> Data { cornerHeight: cornerRadius, transform: nil) - // Background gradient — bright red at top, deeper red at bottom. + // Background gradient, bright red at top, deeper red at bottom. ctx.saveGState() ctx.addPath(path) ctx.clip() @@ -53,7 +53,7 @@ func render(sizePx: Int) -> Data { options: []) ctx.restoreGState() - // Top highlight — subtle glassy sheen. + // Top highlight, subtle glassy sheen. ctx.saveGState() ctx.addPath(path) ctx.clip() @@ -71,10 +71,10 @@ func render(sizePx: Int) -> Data { options: []) ctx.restoreGState() - // Play triangle — equilateral pointing right, centroid at icon centre. + // Play triangle, equilateral pointing right, centroid at icon centre. // The vertices (r, 0), (-r/2, ±r·√3/2) average to (0, 0), so placing - // them relative to (cx, cy) puts the centroid — i.e. the visual - // balance point — exactly at the icon's centre. The tip extends + // them relative to (cx, cy) puts the centroid, i.e. the visual + // balance point, exactly at the icon's centre. The tip extends // further right than the back extends left, which is the "play // button" look people expect (see SF Symbols' play.fill). let cx = s / 2