From 99e1f54bbe7d9da95f9fcdf39925001334561b63 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Feb 2026 02:08:02 +0000 Subject: [PATCH 1/7] Add gesture inversion settings to Apple Watch app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Center timer in the middle row with spacers - Add settings gear button to the right of the timer - Create SettingsView with gesture inversion toggle - Add isInverted property to GestureManager with UserDefaults persistence - In inverted mode: counterclockwise → next, clockwise → previous https://claude.ai/code/session_01NwfWjGZ6sYHeWY1c63VyUr --- WatchApp/ContentView.swift | 26 +++++++++++++++++++++++++- WatchApp/GestureManager.swift | 21 +++++++++++++++------ WatchApp/SettingsView.swift | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 7 deletions(-) create mode 100644 WatchApp/SettingsView.swift diff --git a/WatchApp/ContentView.swift b/WatchApp/ContentView.swift index f039168..fd32602 100644 --- a/WatchApp/ContentView.swift +++ b/WatchApp/ContentView.swift @@ -8,6 +8,7 @@ struct ContentView: View { @State private var timerStartDate: Date? @State private var accumulatedTime: TimeInterval = 0 @State private var timerRunning = false + @State private var showSettings = false private func elapsedTime(at date: Date) -> TimeInterval { if timerRunning, let start = timerStartDate { @@ -56,7 +57,7 @@ struct ContentView: View { } .buttonStyle(.plain) - // Timer + gesture toggle row + // Timer + gesture toggle + settings row HStack(spacing: 4) { // Gesture toggle — tap or hardware double-tap to toggle Button { @@ -78,6 +79,8 @@ struct ContentView: View { .buttonStyle(.plain) .modifier(PrimaryGestureShortcut()) + Spacer() + // Timer — tap to start/stop, long press to reset VStack(spacing: 2) { Text(formattedTime(at: context.date)) @@ -100,6 +103,27 @@ struct ContentView: View { .onLongPressGesture { resetTimer() } + + Spacer() + + // Settings button + Button { + showSettings = true + } label: { + Image(systemName: "gearshape.fill") + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.secondary) + .frame(width: 30, height: 30) + .background(Color.white.opacity(0.08)) + .clipShape(Circle()) + } + .buttonStyle(.plain) + } + .sheet(isPresented: $showSettings) { + NavigationStack { + SettingsView() + .environmentObject(gestureManager) + } } // Next slide button diff --git a/WatchApp/GestureManager.swift b/WatchApp/GestureManager.swift index b9a0536..0859609 100644 --- a/WatchApp/GestureManager.swift +++ b/WatchApp/GestureManager.swift @@ -5,9 +5,13 @@ import Combine /// Detects wrist flick gestures using CoreMotion for hands-free slide control. /// -/// Gesture mapping: -/// - Flick wrist forward (away from body) → Next slide -/// - Flick wrist backward (toward body) → Previous slide +/// Default gesture mapping: +/// - Flick wrist forward (clockwise, away from body) → Next slide +/// - Flick wrist backward (counterclockwise, toward body) → Previous slide +/// +/// Inverted gesture mapping: +/// - Counterclockwise → Next slide +/// - Clockwise → Previous slide /// /// Uses the gyroscope rotation rate around the x-axis, which corresponds /// to wrist flexion/extension — the natural "flick forward" and "pull back" motion. @@ -17,6 +21,9 @@ class GestureManager: ObservableObject { @Published var isEnabled = false @Published var lastGesture: DetectedGesture? + @Published var isInverted: Bool { + didSet { UserDefaults.standard.set(isInverted, forKey: "gestureInverted") } + } enum DetectedGesture: Equatable { case next @@ -49,6 +56,7 @@ class GestureManager: ObservableObject { // MARK: - Initialization init() { + self.isInverted = UserDefaults.standard.bool(forKey: "gestureInverted") motionQueue.name = "com.dou.clicker.gesture" motionQueue.maxConcurrentOperationCount = 1 } @@ -109,9 +117,10 @@ class GestureManager: ObservableObject { guard now.timeIntervalSince(lastTriggerTime) >= cooldownInterval else { return } lastTriggerTime = now - // Positive x rotation = wrist flick forward = next slide - // Negative x rotation = wrist flick backward = previous slide - let gesture: DetectedGesture = rotationX > 0 ? .next : .previous + // Positive x rotation = wrist flick forward, Negative = backward + // When inverted: counterclockwise (negative) = next, clockwise (positive) = previous + let isInverted = self.isInverted + let gesture: DetectedGesture = (rotationX > 0) != isInverted ? .next : .previous DispatchQueue.main.async { [weak self] in guard let self else { return } diff --git a/WatchApp/SettingsView.swift b/WatchApp/SettingsView.swift new file mode 100644 index 0000000..3b1eeb2 --- /dev/null +++ b/WatchApp/SettingsView.swift @@ -0,0 +1,32 @@ +import SwiftUI + +struct SettingsView: View { + @EnvironmentObject var gestureManager: GestureManager + + var body: some View { + List { + Section { + Toggle(isOn: $gestureManager.isInverted) { + VStack(alignment: .leading, spacing: 2) { + Text("Invert Gestures") + .font(.system(size: 15)) + } + } + } footer: { + Text(gestureManager.isInverted + ? "Counterclockwise → Next\nClockwise → Previous" + : "Clockwise → Next\nCounterclockwise → Previous") + .font(.system(size: 11)) + .foregroundColor(.secondary) + } + } + .navigationTitle("Settings") + } +} + +#Preview { + NavigationStack { + SettingsView() + .environmentObject(GestureManager()) + } +} From 90145aa7dd25f1f56204831e6e64c91ffff4799c Mon Sep 17 00:00:00 2001 From: donny-son Date: Sun, 1 Mar 2026 10:37:01 +0900 Subject: [PATCH 2/7] add gesture lock with auto activation in apple watch through settings --- WatchApp/ClickerWatchApp.swift | 8 +++-- WatchApp/ContentView.swift | 42 ++++++++++++++++-------- WatchApp/GestureManager.swift | 60 +++++++++++++++++++++++++++++++--- WatchApp/SettingsView.swift | 26 +++++++++++++++ 4 files changed, 116 insertions(+), 20 deletions(-) diff --git a/WatchApp/ClickerWatchApp.swift b/WatchApp/ClickerWatchApp.swift index 312c777..4fd8a36 100644 --- a/WatchApp/ClickerWatchApp.swift +++ b/WatchApp/ClickerWatchApp.swift @@ -25,9 +25,13 @@ struct ClickerWatchApp: App { case .active: sessionManager.start() workoutManager.start() + if gestureManager.autoToggleWithWrist { + gestureManager.start() + } case .inactive: - // Screen dimmed (AOD) — don't change anything - break + if gestureManager.autoToggleWithWrist { + gestureManager.stop() + } case .background: // User explicitly navigated away — stop workout to save battery workoutManager.stop() diff --git a/WatchApp/ContentView.swift b/WatchApp/ContentView.swift index fd32602..547a548 100644 --- a/WatchApp/ContentView.swift +++ b/WatchApp/ContentView.swift @@ -38,9 +38,10 @@ struct ContentView: View { .font(.system(size: 32, weight: .bold)) if gestureManager.lastGesture == .previous { - Image(systemName: "hand.wave.fill") + Image(systemName: gestureManager.isLocked ? "lock.fill" : "hand.wave.fill") .font(.system(size: 16)) .foregroundColor(.yellow) + .opacity(gestureManager.gestureLockEnabled ? gestureManager.lockProgress : 1.0) .offset(x: 40, y: -10) .transition(.opacity) } @@ -49,11 +50,12 @@ struct ContentView: View { .frame(height: geometry.size.height * 0.30) .background( gestureManager.lastGesture == .previous - ? Color.yellow.opacity(0.3) + ? Color.yellow.opacity(gestureManager.gestureLockEnabled ? 0.3 * gestureManager.lockProgress : 0.3) : Color.blue.opacity(isLuminanceReduced ? 0.1 : 0.3) ) .clipShape(RoundedRectangle(cornerRadius: 12)) .animation(.easeOut(duration: 0.2), value: gestureManager.lastGesture) + .animation(.linear(duration: 0.05), value: gestureManager.lockProgress) } .buttonStyle(.plain) @@ -64,17 +66,29 @@ struct ContentView: View { gestureManager.toggle() WKInterfaceDevice.current().play(gestureManager.isEnabled ? .stop : .start) } label: { - Image(systemName: gestureManager.isEnabled ? "hand.wave.fill" : "hand.wave") - .font(.system(size: 14, weight: .medium)) - .foregroundColor(gestureManager.isEnabled ? .yellow : .secondary) - .frame(width: 30, height: 30) - .background( - gestureManager.isEnabled + ZStack { + Image(systemName: gestureManager.isEnabled ? "hand.wave.fill" : "hand.wave") + .font(.system(size: 14, weight: .medium)) + .foregroundColor(gestureManager.isEnabled ? .yellow : .secondary) + + if gestureManager.isLocked { + Image(systemName: "lock.fill") + .font(.system(size: 8, weight: .bold)) + .foregroundColor(.orange) + .offset(x: 8, y: -8) + } + } + .frame(width: 30, height: 30) + .background( + gestureManager.isLocked + ? Color.orange.opacity(0.2 * gestureManager.lockProgress) + : gestureManager.isEnabled ? Color.yellow.opacity(0.15) : Color.white.opacity(0.08) - ) - .clipShape(Circle()) - .animation(.easeOut(duration: 0.2), value: gestureManager.isEnabled) + ) + .clipShape(Circle()) + .animation(.easeOut(duration: 0.2), value: gestureManager.isEnabled) + .animation(.linear(duration: 0.05), value: gestureManager.lockProgress) } .buttonStyle(.plain) .modifier(PrimaryGestureShortcut()) @@ -136,9 +150,10 @@ struct ContentView: View { .font(.system(size: 32, weight: .bold)) if gestureManager.lastGesture == .next { - Image(systemName: "hand.wave.fill") + Image(systemName: gestureManager.isLocked ? "lock.fill" : "hand.wave.fill") .font(.system(size: 16)) .foregroundColor(.yellow) + .opacity(gestureManager.gestureLockEnabled ? gestureManager.lockProgress : 1.0) .offset(x: 40, y: -10) .transition(.opacity) } @@ -147,11 +162,12 @@ struct ContentView: View { .frame(height: geometry.size.height * 0.30) .background( gestureManager.lastGesture == .next - ? Color.yellow.opacity(0.3) + ? Color.yellow.opacity(gestureManager.gestureLockEnabled ? 0.3 * gestureManager.lockProgress : 0.3) : Color.blue.opacity(isLuminanceReduced ? 0.1 : 0.3) ) .clipShape(RoundedRectangle(cornerRadius: 12)) .animation(.easeOut(duration: 0.2), value: gestureManager.lastGesture) + .animation(.linear(duration: 0.05), value: gestureManager.lockProgress) } .buttonStyle(.plain) } diff --git a/WatchApp/GestureManager.swift b/WatchApp/GestureManager.swift index 0859609..4af5cc0 100644 --- a/WatchApp/GestureManager.swift +++ b/WatchApp/GestureManager.swift @@ -24,6 +24,14 @@ class GestureManager: ObservableObject { @Published var isInverted: Bool { didSet { UserDefaults.standard.set(isInverted, forKey: "gestureInverted") } } + @Published var autoToggleWithWrist: Bool { + didSet { UserDefaults.standard.set(autoToggleWithWrist, forKey: "gestureAutoToggle") } + } + @Published var gestureLockEnabled: Bool { + didSet { UserDefaults.standard.set(gestureLockEnabled, forKey: "gestureLockEnabled") } + } + @Published var isLocked: Bool = false + @Published var lockProgress: CGFloat = 0.0 enum DetectedGesture: Equatable { case next @@ -44,6 +52,9 @@ class GestureManager: ObservableObject { /// Minimum time between gesture triggers to prevent double-fires. private let cooldownInterval: TimeInterval = 0.8 + /// Duration of the gesture lock period. + private let lockDuration: TimeInterval = 3.0 + /// Motion update frequency in Hz. private let updateFrequency: Double = 50.0 @@ -52,11 +63,14 @@ class GestureManager: ObservableObject { private let motionManager = CMMotionManager() private var lastTriggerTime: Date = .distantPast private let motionQueue = OperationQueue() + private var lockTimer: Timer? // MARK: - Initialization init() { self.isInverted = UserDefaults.standard.bool(forKey: "gestureInverted") + self.autoToggleWithWrist = UserDefaults.standard.bool(forKey: "gestureAutoToggle") + self.gestureLockEnabled = UserDefaults.standard.bool(forKey: "gestureLockEnabled") motionQueue.name = "com.dou.clicker.gesture" motionQueue.maxConcurrentOperationCount = 1 } @@ -93,6 +107,10 @@ class GestureManager: ObservableObject { DispatchQueue.main.async { self.isEnabled = false self.lastGesture = nil + self.lockTimer?.invalidate() + self.lockTimer = nil + self.isLocked = false + self.lockProgress = 0 } print("⌚ Gesture detection stopped") } @@ -114,13 +132,15 @@ class GestureManager: ObservableObject { guard abs(rotationX) > rotationThreshold else { return } let now = Date() - guard now.timeIntervalSince(lastTriggerTime) >= cooldownInterval else { return } + let effectiveCooldown = gestureLockEnabled ? lockDuration : cooldownInterval + guard now.timeIntervalSince(lastTriggerTime) >= effectiveCooldown else { return } lastTriggerTime = now // Positive x rotation = wrist flick forward, Negative = backward // When inverted: counterclockwise (negative) = next, clockwise (positive) = previous let isInverted = self.isInverted let gesture: DetectedGesture = (rotationX > 0) != isInverted ? .next : .previous + let lockEnabled = self.gestureLockEnabled DispatchQueue.main.async { [weak self] in guard let self else { return } @@ -144,12 +164,42 @@ class GestureManager: ObservableObject { self.onPreviousSlide?() } - // Clear visual indicator after a short delay - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - if self.lastGesture == gesture { - self.lastGesture = nil + if lockEnabled { + self.startLockCountdown(for: gesture) + } else { + // Clear visual indicator after a short delay + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + if self.lastGesture == gesture { + self.lastGesture = nil + } } } } } + + // MARK: - Gesture Lock + + private func startLockCountdown(for gesture: DetectedGesture) { + lockTimer?.invalidate() + isLocked = true + lockProgress = 1.0 + + let startTime = Date() + lockTimer = Timer.scheduledTimer(withTimeInterval: 1.0 / 30.0, repeats: true) { [weak self] timer in + guard let self else { + timer.invalidate() + return + } + let elapsed = Date().timeIntervalSince(startTime) + if elapsed >= self.lockDuration { + timer.invalidate() + self.lockTimer = nil + self.isLocked = false + self.lockProgress = 0 + self.lastGesture = nil + } else { + self.lockProgress = CGFloat(1.0 - elapsed / self.lockDuration) + } + } + } } diff --git a/WatchApp/SettingsView.swift b/WatchApp/SettingsView.swift index 3b1eeb2..765cc75 100644 --- a/WatchApp/SettingsView.swift +++ b/WatchApp/SettingsView.swift @@ -5,6 +5,19 @@ struct SettingsView: View { var body: some View { List { + Section { + Toggle(isOn: $gestureManager.gestureLockEnabled) { + VStack(alignment: .leading, spacing: 2) { + Text("Gesture Lock") + .font(.system(size: 15)) + } + } + } footer: { + Text("After a gesture, lock out further gestures for 3 seconds to prevent accidental triggers") + .font(.system(size: 11)) + .foregroundColor(.secondary) + } + Section { Toggle(isOn: $gestureManager.isInverted) { VStack(alignment: .leading, spacing: 2) { @@ -19,6 +32,19 @@ struct SettingsView: View { .font(.system(size: 11)) .foregroundColor(.secondary) } + + Section { + Toggle(isOn: $gestureManager.autoToggleWithWrist) { + VStack(alignment: .leading, spacing: 2) { + Text("Auto-toggle with Wrist") + .font(.system(size: 15)) + } + } + } footer: { + Text("Gestures enable on wrist raise and disable on wrist lower") + .font(.system(size: 11)) + .foregroundColor(.secondary) + } } .navigationTitle("Settings") } From 9b0b635bd85e6953a354bdfd78797faf00a35c97 Mon Sep 17 00:00:00 2001 From: donny-son Date: Sun, 1 Mar 2026 11:18:02 +0900 Subject: [PATCH 3/7] remove mkdocs documentation and use github wiki --- .python-version | 1 - docs/development/architecture.md | 324 ------------------- docs/development/building.md | 258 --------------- docs/development/extending.md | 254 --------------- docs/development/structure.md | 234 -------------- docs/getting-started/first-run.md | 140 --------- docs/getting-started/installation.md | 68 ---- docs/index.md | 89 ------ docs/privacy.md | 100 ------ docs/reference/keycodes.md | 161 ---------- docs/reference/troubleshooting.md | 227 -------------- main.py | 6 - mkdocs.yml | 80 ----- pyproject.toml | 9 - uv.lock | 452 --------------------------- 15 files changed, 2403 deletions(-) delete mode 100644 .python-version delete mode 100644 docs/development/architecture.md delete mode 100644 docs/development/building.md delete mode 100644 docs/development/extending.md delete mode 100644 docs/development/structure.md delete mode 100644 docs/getting-started/first-run.md delete mode 100644 docs/getting-started/installation.md delete mode 100644 docs/index.md delete mode 100644 docs/privacy.md delete mode 100644 docs/reference/keycodes.md delete mode 100644 docs/reference/troubleshooting.md delete mode 100644 main.py delete mode 100644 mkdocs.yml delete mode 100644 pyproject.toml delete mode 100644 uv.lock diff --git a/.python-version b/.python-version deleted file mode 100644 index 24ee5b1..0000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.13 diff --git a/docs/development/architecture.md b/docs/development/architecture.md deleted file mode 100644 index a55e283..0000000 --- a/docs/development/architecture.md +++ /dev/null @@ -1,324 +0,0 @@ -# Architecture - -## System Overview - -Clicker uses a client-server model over Apple's MultipeerConnectivity framework, with Apple Watch support via WatchConnectivity: - -- **Mac (Server)**: Advertises presence, accepts connections, executes keystrokes -- **iPhone (Client)**: Browses for servers, initiates connections, sends commands -- **Watch (Companion)**: Sends commands to iPhone via WatchConnectivity, which relays to Mac. Supports hands-free wrist gesture control via CoreMotion - -```mermaid -flowchart TB - subgraph Watch["Apple Watch App"] - WUI[SwiftUI Views] - WCM[WatchConnectionManager] - WCS[WCSession] - GM[GestureManager] - WM[WorkoutManager] - end - - subgraph iPhone["iPhone App"] - UI[SwiftUI Views] - ICM[iPhoneConnectionManager] - Browser[MCNearbyServiceBrowser] - Timer[PresentationTimer] - Sub[SubscriptionManager] - end - - subgraph Mac["Mac App"] - MenuBar[MenuBarExtra] - MCM[MacConnectionManager] - Advertiser[MCNearbyServiceAdvertiser] - KS[KeystrokeSender] - CGE[CGEvent API] - end - - subgraph Target["Presentation App"] - Keynote[Keynote / PowerPoint / etc.] - end - - WUI --> WCM - WUI --> GM - WUI --> WM - GM -->|gesture detected| WCM - WCM --> WCS - WCS <-->|WatchConnectivity| ICM - UI --> ICM - UI --> Timer - UI --> Sub - ICM --> Browser - Browser <-->|MultipeerConnectivity| Advertiser - Advertiser --> MCM - MCM --> KS - KS --> CGE - CGE --> Keynote -``` - ---- - -## Communication Protocol - -### Service Discovery - -Both apps use the same service type for discovery: - -```swift -let serviceType = "clicker" // Resolves to _clicker._tcp and _clicker._udp -``` - -The Mac advertises this service, and the iPhone browses for it. - -### Message Format - -Commands are sent as JSON-encoded `RemoteCommand` values: - -```swift -enum RemoteCommand: String, Codable { - case next = "next" - case previous = "previous" -} - -// Sent over the wire as: -// {"rawValue": "next"} -``` - -### Connection Flow - -```mermaid -sequenceDiagram - participant iPhone - participant Mac - participant Keynote - - Note over Mac: App launches - Mac->>Mac: Start MCNearbyServiceAdvertiser - - Note over iPhone: App launches - iPhone->>iPhone: Start MCNearbyServiceBrowser - iPhone->>Mac: Discover advertised service - iPhone->>Mac: Send invitation to connect - Mac->>iPhone: Accept invitation - Note over iPhone,Mac: MCSession established - - loop User interaction - iPhone->>Mac: Send RemoteCommand (JSON) - Mac->>Mac: Decode command - Mac->>Keynote: Inject keystroke via CGEvent - end - - iPhone->>Mac: Disconnect - Note over Mac: Return to advertising -``` - ---- - -## Mac App Architecture - -### Menu Bar Integration - -The Mac app uses `MenuBarExtra` to run entirely in the menu bar: - -```swift -@main -struct ClickerMacApp: App { - var body: some Scene { - MenuBarExtra("Clicker", systemImage: "rectangle.inset.filled.and.cursorarrow") { - ContentView() - } - .menuBarExtraStyle(.window) - } -} -``` - -The `LSUIElement: true` Info.plist key prevents a Dock icon. - -### Keystroke Injection - -`KeystrokeSender` uses the Core Graphics `CGEvent` API: - -```swift -func sendKeystroke(_ keyCode: UInt16) { - let source = CGEventSource(stateID: .hidSystemState) - - // Key down - let keyDown = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: true) - keyDown?.post(tap: .cghidEventTap) - - // Key up - let keyUp = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: false) - keyUp?.post(tap: .cghidEventTap) -} -``` - -!!! warning "Accessibility Permission" - `CGEvent` posting requires the app to be granted Accessibility permission in System Settings. - ---- - -## Apple Watch Architecture - -The Watch app acts as a lightweight remote that relays commands through the iPhone, with support for hands-free gesture control. - -### Communication Chain - -``` -Watch → (WCSession.sendMessage) → iPhone → (MCSession.send) → Mac -``` - -The Watch never connects directly to the Mac. The iPhone acts as a bridge. - -### Wrist Gesture Control - -The `GestureManager` uses CoreMotion to detect wrist flick gestures for hands-free slide navigation: - -- **Flick forward** (wrist away from body) → Next slide -- **Flick backward** (wrist toward body) → Previous slide -- Uses gyroscope rotation rate around the x-axis (wrist flexion/extension) -- Threshold: 3.0 rad/s to filter out incidental motion -- Cooldown: 0.8 seconds between triggers to prevent double-fires -- Motion updates sampled at 50 Hz -- Double haptic pulse (directionUp/directionDown) confirms each gesture -- Toggle on/off via the hand wave button or hardware double-tap (watchOS 11+) - -### Workout Session (Stay Active) - -The `WorkoutManager` uses `HKWorkoutSession` to keep the Watch app visible and active during presentations. Without this, watchOS would dismiss the app when the user lowers their wrist, breaking gesture detection. The workout session auto-restarts on expiration. - -!!! note "HealthKit Usage" - Clicker does not read or store any health data. HealthKit is used solely to maintain an active workout session that prevents the system from suspending the app. - -### Always-On Display - -When connected to a Mac, the iPhone disables the idle timer (`UIApplication.shared.isIdleTimerDisabled = true`). This prevents the screen from locking, which would suspend the app and drop the MultipeerConnectivity session. Auto-lock resumes when you disconnect. - -The Watch UI detects `isLuminanceReduced` to adjust opacity in always-on display mode. - -### Watch Timer - -The Watch has its own independent timer: - -- **Tap** the timer to start/stop -- **Long press** the timer to reset (with `.notification` haptic feedback) -- Monospaced display with color coding (green when running, white when stopped) - ---- - -## iPhone App Architecture - -### View Hierarchy - -```mermaid -graph TD - App[ClickerApp] --> Gate[SubscriptionGateView] - Gate -->|Loading| Progress[ProgressView] - Gate -->|Trial/Subscribed| Content[ContentView] - Gate -->|Expired| Paywall[PaywallView] - - Content -->|Not Connected| Connection[ConnectionView] - Content -->|Connected| Remote[RemoteControlView] - - Remote --> StatusBar[StatusBarView] - Remote --> Buttons[SlideButton x2] - Remote --> TimerView[TimerView] - - TimerView --> Settings[TimerSettingsView] -``` - -### State Management - -The app uses SwiftUI's native state management: - -| State | Type | Scope | -|-------|------|-------| -| Connection | `@StateObject` + `ObservableObject` | App-wide | -| Timer | `@StateObject` + `ObservableObject` | App-wide | -| Subscription | `@State` + `@Observable` | App-wide via Environment | -| UI State | `@State` | Per-view | - -### Subscription Flow - -```mermaid -stateDiagram-v2 - [*] --> NotDetermined: App Launch - - NotDetermined --> Trial: First Launch - NotDetermined --> Subscribed: Has Entitlement - NotDetermined --> Expired: Trial Ended - - Trial --> Expired: Day 8+ - Trial --> Subscribed: Purchase - - Expired --> Subscribed: Purchase - Subscribed --> Expired: Subscription Ends - - state Trial { - [*] --> ShowApp - ShowApp --> ShowBanner: Display Days Remaining - } - - state Subscribed { - [*] --> FullAccess - } - - state Expired { - [*] --> ShowPaywall - } -``` - ---- - -## Data Persistence - -### Trial Tracking (Keychain) - -Trial start date is stored in Keychain to survive app reinstallation: - -```swift -class TrialTracker { - private let keychainKey = "com.dou.clicker.trial.start" - - func startTrial() { - let startDate = Date() - // Store in Keychain with kSecAttrAccessibleAfterFirstUnlock - } - - var daysRemaining: Int { - // Calculate from stored start date - } -} -``` - -### Subscription State (StoreKit) - -Subscription status is determined from StoreKit entitlements: - -```swift -func checkEntitlements() async { - for await result in Transaction.currentEntitlements { - // Verify and extract expiration date - } -} -``` - ---- - -## Thread Safety - -Both apps use `@MainActor` for thread-safe UI updates: - -```swift -@MainActor -class MacConnectionManager: NSObject, ObservableObject { - @Published var isConnected = false - // All published properties update on main thread -} -``` - -MultipeerConnectivity delegates are dispatched to the main queue: - -```swift -browser = MCNearbyServiceBrowser(peer: peerID, serviceType: serviceType) -browser.delegate = self -// Delegate methods called on main queue by default -``` diff --git a/docs/development/building.md b/docs/development/building.md deleted file mode 100644 index eddcc05..0000000 --- a/docs/development/building.md +++ /dev/null @@ -1,258 +0,0 @@ -# Building from Source - -This guide covers building Clicker from source code using command-line tools. - -## Prerequisites - -| Requirement | Version | Installation | -|-------------|---------|--------------| -| macOS | 14.0+ (Sonoma) | — | -| Xcode | 16.0+ | App Store or [developer.apple.com](https://developer.apple.com/xcode/) | -| XcodeGen | Latest | `brew install xcodegen` | -| Apple Developer Account | — | Required for code signing | - -### Install XcodeGen - -```bash -brew install xcodegen -``` - -XcodeGen generates the Xcode project from `project.yml`, keeping build configuration in version control. - ---- - -## Quick Build - -```bash -# Clone the repository -git clone https://github.com/douinc/clicker.git -cd clicker - -# Generate Xcode project -xcodegen generate - -# Build Mac app -xcodebuild -scheme ClickerMac -configuration Release build - -# Run Mac app -open ~/Library/Developer/Xcode/DerivedData/Clicker-*/Build/Products/Release/Clicker.app -``` - ---- - -## Detailed Build Instructions - -### Generate Xcode Project - -```bash -xcodegen generate -``` - -This reads `project.yml` and generates `Clicker.xcodeproj`. Always run this after: - -- Cloning the repository -- Pulling changes that modify `project.yml` -- Changing build settings - -!!! warning "Don't Edit .xcodeproj" - The Xcode project is generated. Any manual changes will be lost when you run `xcodegen generate`. - -### Build Mac App - -=== "Debug Build" - - ```bash - xcodebuild -scheme ClickerMac build - ``` - -=== "Release Build" - - ```bash - xcodebuild -scheme ClickerMac -configuration Release build - ``` - -=== "Clean Build" - - ```bash - xcodebuild -scheme ClickerMac clean build - ``` - -**Output location:** -``` -~/Library/Developer/Xcode/DerivedData/Clicker-*/Build/Products/Debug/Clicker.app -~/Library/Developer/Xcode/DerivedData/Clicker-*/Build/Products/Release/Clicker.app -``` - -### Build iOS App - -=== "Simulator" - - ```bash - xcodebuild -scheme ClickeriOS \ - -destination 'generic/platform=iOS Simulator' \ - build - ``` - -=== "Specific Simulator" - - ```bash - xcodebuild -scheme ClickeriOS \ - -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.2' \ - build - ``` - -=== "Physical Device" - - ```bash - # Find your device ID - xcrun devicectl list devices - - # Build for device - xcodebuild -scheme ClickeriOS \ - -destination 'id=YOUR_DEVICE_ID' \ - build - ``` - -### Install on iPhone - -```bash -# Find device ID -xcrun devicectl list devices - -# Install the app -xcrun devicectl device install app \ - --device YOUR_DEVICE_ID \ - ~/Library/Developer/Xcode/DerivedData/Clicker-*/Build/Products/Debug-iphoneos/Clicker.app - -# Launch the app -xcrun devicectl device process launch \ - --device YOUR_DEVICE_ID \ - com.dou.clicker-ios -``` - ---- - -## Code Signing - -### Find Your Team ID - -```bash -security find-identity -v -p codesigning | grep "Apple Development" -``` - -The Team ID is the 10-character code in parentheses, e.g., `HD35YQ72U4`. - -### Configure Signing - -Edit `project.yml` and update the `DEVELOPMENT_TEAM`: - -```yaml -settings: - base: - DEVELOPMENT_TEAM: YOUR_TEAM_ID # (1)! -``` - -1. Replace with your actual Team ID - -Then regenerate the project: - -```bash -xcodegen generate -``` - ---- - -## Build Both Apps in Parallel - -```bash -xcodegen generate - -# Build both simultaneously -xcodebuild -scheme ClickerMac build & -xcodebuild -scheme ClickeriOS -destination 'generic/platform=iOS Simulator' build & -wait - -echo "Both builds complete" -``` - ---- - -## Useful Commands - -### List Available Schemes - -```bash -xcodebuild -project Clicker.xcodeproj -list -``` - -### List Build Destinations - -```bash -xcodebuild -scheme ClickeriOS -showdestinations -``` - -### List Connected Devices - -```bash -xcrun devicectl list devices -``` - -### List Available Simulators - -```bash -xcrun simctl list devices available -``` - -### Check Built Info.plist - -```bash -find ~/Library/Developer/Xcode/DerivedData/Clicker-*/Build/Products \ - -name "Info.plist" \ - -exec plutil -p {} \; -``` - ---- - -## Common Build Errors - -### "Signing requires a development team" - -Add your Team ID to `project.yml`: - -```yaml -settings: - base: - DEVELOPMENT_TEAM: YOUR_TEAM_ID -``` - -Then run `xcodegen generate`. - -### "Unable to find destination" - -List available destinations and use an exact match: - -```bash -xcodebuild -scheme ClickeriOS -showdestinations -``` - -### "No such module" errors - -Clean and rebuild: - -```bash -xcodebuild -scheme ClickeriOS clean -xcodebuild -scheme ClickeriOS build -``` - -### MultipeerConnectivity not working - -Ensure `project.yml` has the required Info.plist entries under `info.properties`: - -```yaml -info: - properties: - NSBonjourServices: - - _clicker._tcp - - _clicker._udp - NSLocalNetworkUsageDescription: "..." -``` diff --git a/docs/development/extending.md b/docs/development/extending.md deleted file mode 100644 index 5b4e09c..0000000 --- a/docs/development/extending.md +++ /dev/null @@ -1,254 +0,0 @@ -# Extending Clicker - -This guide covers how to add new features and commands to Clicker. - -## Adding New Commands - -### 1. Define the Command - -Add a new case to `RemoteCommand` in `Shared/RemoteCommand.swift`: - -```swift -enum RemoteCommand: String, Codable { - case next = "next" - case previous = "previous" - case blackScreen = "black" // New! - case whiteScreen = "white" // New! - case startPresentation = "start" // New! - - var keyCode: UInt16 { - switch self { - case .next: return 124 // Right Arrow - case .previous: return 123 // Left Arrow - case .blackScreen: return 11 // B key - case .whiteScreen: return 13 // W key - case .startPresentation: return 36 // Return key - } - } -} -``` - -### 2. Add UI in iPhone App - -Add a button to trigger the command in `iPhoneApp/PresentationRemoteiPhoneApp.swift`: - -```swift -// In RemoteControlView or a new toolbar -Button { - connectionManager.sendCommand(.blackScreen) -} label: { - Label("Black", systemImage: "rectangle.fill") -} -``` - -### 3. Test - -1. Build both apps -2. Connect iPhone to Mac -3. Tap the new button -4. Verify the keystroke is received - -!!! tip "No Mac Changes Needed" - The Mac app automatically handles any `RemoteCommand` — it just reads the `keyCode` and sends it. - ---- - -## Adding Modifier Keys - -To send keystrokes with modifiers (Shift, Command, etc.), modify `KeystrokeSender`: - -```swift -func sendKeystroke(_ keyCode: UInt16, modifiers: CGEventFlags = []) { - let source = CGEventSource(stateID: .hidSystemState) - - let keyDown = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: true) - keyDown?.flags = modifiers // Add modifiers - keyDown?.post(tap: .cghidEventTap) - - let keyUp = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: false) - keyUp?.flags = modifiers - keyUp?.post(tap: .cghidEventTap) -} -``` - -Usage: - -```swift -// Command+Shift+F for fullscreen -sendKeystroke(3, modifiers: [.maskCommand, .maskShift]) -``` - -### Common Modifier Flags - -| Modifier | Flag | -|----------|------| -| Shift | `.maskShift` | -| Control | `.maskControl` | -| Option/Alt | `.maskAlternate` | -| Command | `.maskCommand` | - ---- - -## Adding Timer Features - -### Custom Haptic Patterns - -Modify `PresentationTimer` to add new haptic patterns: - -```swift -func customHaptic() { - let generator = UIImpactFeedbackGenerator(style: .heavy) - generator.prepare() - - // Pattern: tap-tap-pause-tap - generator.impactOccurred() - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - generator.impactOccurred() - } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - generator.impactOccurred() - } -} -``` - -### New Timer Presets - -Add presets in `TimerConfig`: - -```swift -static let durationPresets: [(name: String, duration: TimeInterval?)] = [ - ("No Limit", nil), - ("3 minutes", 180), // New! - ("5 minutes", 300), - ("10 minutes", 600), - // ... -] -``` - ---- - -## Adding New Screens - -### SwiftUI Pattern - -Follow the existing pattern for new views: - -```swift -struct NewFeatureView: View { - @ObservedObject var connectionManager: iPhoneConnectionManager - - var body: some View { - VStack { - // Your UI here - } - .background(.ultraThinMaterial) // Liquid glass style - .preferredColorScheme(.dark) // Dark mode - } -} -``` - -### Navigation - -Add to the view hierarchy in `ContentView`: - -```swift -var body: some View { - if connectionManager.isConnected { - RemoteControlView(...) - } else if showNewFeature { - NewFeatureView(...) - } else { - ConnectionView(...) - } -} -``` - ---- - -## Subscription Features - -### Gating Features - -Use `SubscriptionManager` to gate premium features: - -```swift -struct PremiumFeatureView: View { - @Environment(SubscriptionManager.self) var subscriptionManager - - var body: some View { - if subscriptionManager.hasAccess { - // Premium content - } else { - // Upgrade prompt - PaywallView() - } - } -} -``` - -### Adding Products - -1. Add product ID to `Products.storekit` for testing -2. Create the product in App Store Connect -3. Update `SubscriptionManager.productIDs` - ---- - -## Testing Changes - -### StoreKit Testing - -Use the StoreKit configuration file for local testing: - -1. In Xcode, select scheme → Edit Scheme -2. Under Options, set StoreKit Configuration to `Products.storekit` -3. Build and run — purchases use sandbox - -### MultipeerConnectivity Testing - -Test on physical devices when possible. Simulator limitations: - -- ✅ Mac app works in simulator -- ⚠️ iOS simulator has limited MultipeerConnectivity support -- ✅ Best to test iPhone app on physical device - ---- - -## Code Style - -### SwiftUI Conventions - -```swift -// Use @StateObject for owned objects -@StateObject private var viewModel = ViewModel() - -// Use @ObservedObject for injected objects -@ObservedObject var connectionManager: ConnectionManager - -// Use @Environment for app-wide state -@Environment(SubscriptionManager.self) var subscriptionManager -``` - -### File Organization - -```swift -// MARK: - View Name -struct MyView: View { - // MARK: Properties - @State private var value = false - - // MARK: Body - var body: some View { ... } - - // MARK: Subviews - private var header: some View { ... } - - // MARK: Methods - private func handleTap() { ... } -} - -// MARK: - Preview -#Preview { - MyView() -} -``` diff --git a/docs/development/structure.md b/docs/development/structure.md deleted file mode 100644 index eb26405..0000000 --- a/docs/development/structure.md +++ /dev/null @@ -1,234 +0,0 @@ -# Project Structure - -## Directory Layout - -``` -clicker/ -├── project.yml # XcodeGen configuration (source of truth) -├── Shared/ # Code shared between both apps -│ └── RemoteCommand.swift # Command protocol -├── MacApp/ # macOS menu bar application -│ ├── PresentationRemoteMacApp.swift -│ ├── MacConnectionManager.swift -│ ├── KeystrokeSender.swift -│ ├── Info.plist -│ └── ClickerMac.entitlements -├── WatchApp/ # watchOS companion app -│ ├── ClickerWatchApp.swift -│ ├── ContentView.swift -│ ├── GestureManager.swift -│ ├── WorkoutManager.swift -│ ├── ExtendedSessionManager.swift -│ ├── WatchConnectionManager.swift -│ ├── ClickerWatch.entitlements -│ └── Info.plist -├── iPhoneApp/ # iOS remote control app -│ ├── PresentationRemoteiPhoneApp.swift -│ ├── iPhoneConnectionManager.swift -│ ├── PresentationTimer.swift -│ ├── SubscriptionManager.swift -│ ├── TrialTracker.swift -│ ├── SubscriptionStatus.swift -│ ├── PaywallView.swift -│ ├── Products.storekit -│ └── Info.plist -├── Clicker.xcodeproj/ # Generated by XcodeGen (do not edit) -├── docs/ # Documentation (MkDocs) -└── plans/ # Feature planning documents -``` - ---- - -## Key Files - -### `project.yml` - -The single source of truth for build configuration. Defines: - -- All three targets (ClickerMac, ClickeriOS, ClickerWatch) -- Build settings and signing -- Info.plist properties -- Capabilities and entitlements - -!!! warning "Generated Project" - Never edit `Clicker.xcodeproj` directly. Changes will be overwritten when you run `xcodegen generate`. - -### Shared Code - -#### `RemoteCommand.swift` - -Defines the command protocol used for communication between iPhone and Mac: - -```swift -enum RemoteCommand: String, Codable { - case next = "next" - case previous = "previous" - - var keyCode: UInt16 { - switch self { - case .next: return 124 // Right Arrow - case .previous: return 123 // Left Arrow - } - } -} -``` - -Commands are JSON-encoded and sent over MultipeerConnectivity. - ---- - -## Mac App Files - -### `PresentationRemoteMacApp.swift` - -Main app entry point and SwiftUI views: - -- Menu bar interface using `MenuBarExtra` -- Connection status display -- Settings and controls - -### `MacConnectionManager.swift` - -Handles MultipeerConnectivity advertising: - -- Creates and manages `MCNearbyServiceAdvertiser` -- Accepts connection requests from iPhone -- Receives and decodes commands -- Delegates keystroke execution - -### `KeystrokeSender.swift` - -Injects keystrokes into the frontmost application: - -- Uses `CGEvent` API for keystroke simulation -- Requires Accessibility permission -- Maps `RemoteCommand` to key codes - ---- - -## iPhone App Files - -### `PresentationRemoteiPhoneApp.swift` - -Main app with all UI components: - -- `ClickerApp` — App entry point -- `SubscriptionGateView` — Access control based on subscription status -- `ContentView` — Connection/remote switching -- `ConnectionView` — Mac discovery and connection -- `RemoteControlView` — Slide navigation buttons -- `TimerView` — Presentation timer display -- `TimerSettingsView` — Timer configuration - -### `iPhoneConnectionManager.swift` - -Handles MultipeerConnectivity browsing: - -- Creates and manages `MCNearbyServiceBrowser` -- Discovers available Macs -- Initiates connections -- Sends commands as JSON - -### `PresentationTimer.swift` - -Presentation timer with haptic feedback: - -- Configurable duration and intervals -- `UIImpactFeedbackGenerator` for haptics -- Progress tracking and overtime detection - -### Subscription Files - -| File | Purpose | -|------|---------| -| `SubscriptionManager.swift` | StoreKit 2 integration, purchase handling | -| `TrialTracker.swift` | 7-day trial tracking via Keychain | -| `SubscriptionStatus.swift` | Status enum (trial, subscribed, expired) | -| `PaywallView.swift` | Native `SubscriptionStoreView` presentation | -| `Products.storekit` | StoreKit configuration for testing | - ---- - -## Watch App Files - -### `ClickerWatchApp.swift` - -App entry point for the watchOS companion. Initializes state managers for connection, gestures, workouts, and extended runtime. - -### `ContentView.swift` - -Main watch interface with: - -- Previous/next slide buttons (30% height each for easy tapping) -- Gesture toggle button with visual indicator (yellow hand wave icon) -- Presentation timer (tap to start/stop, long press to reset) -- Connection status indicator -- Always-on display support with luminance reduction -- Hardware double-tap gesture shortcut (watchOS 11+) - -### `GestureManager.swift` - -CoreMotion-based wrist gesture detection for hands-free slide control: - -- Uses gyroscope rotation rate around x-axis (wrist flexion/extension) -- Flick forward (positive x rotation) = next slide -- Flick backward (negative x rotation) = previous slide -- Rotation threshold: 3.0 rad/s to prevent false positives -- Cooldown: 0.8 seconds between triggers -- Motion updates at 50 Hz -- Double haptic pulse confirmation (directionUp/directionDown) - -### `WorkoutManager.swift` - -HealthKit workout session management to keep the app active: - -- Starts an `HKWorkoutSession` to prevent watchOS from dismissing the app on wrist-down -- Maintains reliable gesture control throughout presentations -- Gracefully handles session expiration with auto-restart - -### `ExtendedSessionManager.swift` - -Extended runtime session handling: - -- Uses `WKExtendedRuntimeSession` to extend app runtime beyond default limits -- Handles expiration and automatic restart - -### `WatchConnectionManager.swift` - -Handles WatchConnectivity with the iPhone: - -- Sends slide commands via `WCSession.sendMessage` -- Receives Mac connection status via `applicationContext` -- Retries failed commands up to 3 times (0.5s intervals) - ---- - -## Bundle Identifiers - -| Target | Bundle ID | -|--------|-----------| -| Mac App | `com.dou.clicker-mac` | -| iOS App | `com.dou.clicker-ios` | -| Watch App | `com.dou.clicker-ios.watchkitapp` | - ---- - -## Info.plist Configuration - -Key entries configured in `project.yml`: - -```yaml -info: - properties: - # MultipeerConnectivity requirements - NSBonjourServices: - - _clicker._tcp - - _clicker._udp - NSLocalNetworkUsageDescription: "Clicker needs local network access to connect to your Mac." - - # Mac: Menu bar app (no Dock icon) - LSUIElement: true -``` - -!!! tip "Info.plist Merging" - XcodeGen merges `info.properties` from `project.yml` with the source `Info.plist` files. Always add new keys to `project.yml`, not the source plists. diff --git a/docs/getting-started/first-run.md b/docs/getting-started/first-run.md deleted file mode 100644 index 3b26e00..0000000 --- a/docs/getting-started/first-run.md +++ /dev/null @@ -1,140 +0,0 @@ -# First Run Setup - -## Mac App Setup - -### 1. Launch the App - -The Mac app runs in your **menu bar** — look for the Clicker icon in the top-right of your screen, not the Dock. - -!!! note "No Dock Icon" - Clicker is a menu bar app (`LSUIElement: true`), so it won't appear in your Dock. - -### 2. Grant Accessibility Permission - -The app needs Accessibility permission to send keystrokes to presentation software. - -1. Click the Clicker menu bar icon -2. macOS will prompt for Accessibility access -3. Click **Open System Settings** -4. Navigate to **Privacy & Security → Accessibility** -5. Enable **Clicker** in the list - -```mermaid -sequenceDiagram - participant User - participant Clicker - participant System Settings - - User->>Clicker: Launch app - Clicker->>User: Request Accessibility - User->>System Settings: Open Privacy settings - User->>System Settings: Enable Clicker - System Settings->>Clicker: Permission granted - Clicker->>User: Ready to receive commands -``` - -!!! warning "Permission Required" - Without Accessibility permission, the app cannot send keystrokes. Your presentation software won't receive any commands. - -### 3. Start Listening - -Once permission is granted: - -1. Click the menu bar icon -2. The app will show "Waiting for iPhone to connect..." -3. Your Mac is now discoverable by the iPhone app - ---- - -## iPhone App Setup - -### 1. Launch and Trial - -On first launch: - -- A 7-day free trial starts automatically -- Full access to all features during trial -- Orange banner shows remaining trial days - -### 2. Grant Local Network Permission - -When the app searches for your Mac: - -1. iOS will prompt for Local Network access -2. Tap **Allow** -3. This permission is required for device discovery - -### 3. Connect to Mac - -1. Ensure both devices are on the same WiFi network -2. The iPhone will discover available Macs -3. Tap your Mac's name to connect -4. Wait for "Connected" status - ---- - -## Using the Remote - -Once connected, you'll see the main remote interface: - -``` -┌─────────────────────────┐ -│ ● Connected to Mac │ -├─────────────────────────┤ -│ │ -│ ◀ Previous │ ← Smaller (38.2%) -│ │ -├─────────────────────────┤ -│ │ -│ │ -│ ▶ Next │ ← Larger (61.8%) -│ │ -│ │ -├─────────────────────────┤ -│ 00:00 ▶ ⟳ ⚙ │ -└─────────────────────────┘ -``` - -!!! tip "Golden Ratio Layout" - The Next button is φ (1.618) times larger than Previous — you'll tap it more often, so it's easier to hit. - -### Controls - -| Control | Action | -|---------|--------| -| **Previous** (top) | Send Left Arrow to Mac | -| **Next** (bottom) | Send Right Arrow to Mac | -| **Play/Pause** | Start/stop presentation timer | -| **Reset** | Reset timer to 00:00 | -| **Settings** | Configure timer and haptics | - ---- - -## Presentation Timer - -The timer helps you track time without looking at your phone. - -### Configuring the Timer - -1. Tap the **gear icon** to open settings -2. Set your presentation duration (5-30 minutes or unlimited) -3. Configure haptic interval (30s to 10m) -4. Enable/disable haptic alerts - -### Haptic Feedback Patterns - -| Event | Pattern | When | -|-------|---------|------| -| Interval | Single pulse | Every X minutes (configurable) | -| Halfway | Triple pulse | 50% of duration | -| Time's Up | Long vibration | End of set duration | -| Overtime | Double pulse | Every 30s after time's up | - -### Visual Progress - -The progress bar changes color as time progresses: - -- **Green** — Plenty of time remaining -- **Yellow** — 75% elapsed -- **Orange** — 90% elapsed -- **Red** — Time's up / overtime diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md deleted file mode 100644 index 6274afe..0000000 --- a/docs/getting-started/installation.md +++ /dev/null @@ -1,68 +0,0 @@ -# Installation - -## Mac App - -### Option 1: Download Release - -1. Download the latest `.dmg` from [GitHub Releases](https://github.com/douinc/clicker/releases) -2. Open the `.dmg` file -3. Drag `Clicker.app` to your Applications folder -4. Launch Clicker from Applications - -!!! warning "macOS Gatekeeper" - If macOS says the app "can't be opened because it is from an unidentified developer", right-click the app and select **Open**, then click **Open** in the dialog. - -### Option 2: Build from Source - -See [Building from Source](../development/building.md) for detailed instructions. - -```bash -brew install xcodegen -git clone https://github.com/douinc/clicker.git -cd clicker -xcodegen generate -xcodebuild -scheme ClickerMac -configuration Release build -open ~/Library/Developer/Xcode/DerivedData/Clicker-*/Build/Products/Release/Clicker.app -``` - -## iPhone App - -Download Clicker from the [App Store](https://apps.apple.com/app/clicker). - -!!! info "Subscription Details" - - **Free Trial**: 7 days with full access - - **Subscription**: $4.99/year after trial - - **Restore**: If you've subscribed before, tap "Restore Purchases" - -## Apple Watch App - -The Apple Watch app is installed automatically when you install the iPhone app. Make sure your Watch is paired with your iPhone. - -The Watch app supports: - -- Tap buttons for slide navigation -- Hands-free wrist gesture control (flick forward/back) -- Independent presentation timer -- Haptic feedback on every action - -!!! tip "Gesture Control" - Tap the hand wave icon on the Watch to enable wrist gestures. Flick your wrist forward for next slide, backward for previous. A double haptic pulse confirms each gesture. - -## System Requirements - -| Platform | Minimum Version | -|----------|-----------------| -| macOS | 14.0 (Sonoma) | -| iOS | 18.0 | -| watchOS | 10.0 | - -## Network Requirements - -Both devices must be able to communicate directly: - -- **Same WiFi network** — Most common setup -- **Bluetooth** — Works as fallback when WiFi is unavailable -- **Personal Hotspot** — iPhone hotspot with Mac connected works - -!!! tip "No Internet Required" - Clicker uses Apple's MultipeerConnectivity framework for peer-to-peer communication. No internet connection is needed — the devices communicate directly. diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 6203476..0000000 --- a/docs/index.md +++ /dev/null @@ -1,89 +0,0 @@ -# Clicker - -**Control your presentations from your iPhone — no dongles, no internet required.** - -Clicker is a presentation remote that turns your iPhone into a wireless clicker for your Mac. It uses peer-to-peer networking via Apple's MultipeerConnectivity framework — no cloud servers, no account required, works completely offline. - -```mermaid -graph LR - subgraph Watch - W[ClickerWatch App] - end - subgraph iPhone - A[Clicker App] - end - subgraph Mac - B[Menu Bar App] - end - subgraph Presentation - C[Keynote / PowerPoint / Slides] - end - - W -->|WatchConnectivity| A - A -->|WiFi / Bluetooth| B - B -->|Keystrokes| C -``` - -## Features - -| Feature | Description | -|---------|-------------| -| **Wireless Control** | Navigate slides with large, easy-to-hit touch targets | -| **Peer-to-Peer** | Direct connection via MultipeerConnectivity (WiFi or Bluetooth) | -| **Apple Watch** | Control slides with tap buttons or hands-free wrist gestures | -| **Wrist Gestures** | Flick forward/back to navigate slides with haptic confirmation | -| **Presentation Timer** | Track time with configurable haptic alerts (iPhone and Watch) | -| **Works Offline** | No internet connection needed | -| **Dark Mode** | Stage-friendly interface with liquid glass aesthetic | -| **Universal** | Works with any app that uses arrow keys | - -## Apps - -### Mac App (Free & Open Source) - -The Mac app runs in your menu bar and receives commands from the iPhone app. It injects keystrokes into the frontmost application using macOS Accessibility APIs. - -[Download from GitHub :material-github:](https://github.com/douinc/clicker/releases){ .md-button .md-button--primary } -[Build from Source](development/building.md){ .md-button } - -### iPhone App - -The iPhone app provides the remote control interface with large navigation buttons and a presentation timer with haptic feedback. - -[App Store — Coming Soon :material-apple:](#){ .md-button disabled style="pointer-events:none;opacity:0.5" } - -!!! info "Subscription" - The iPhone app includes a 7-day free trial, then $4.99/year. - -## Quick Start - -1. **Install the Mac app** — Download from releases or build from source -2. **Grant Accessibility permission** — Required for keystroke injection -3. **Install the iPhone app** — Download from App Store -4. **Connect** — Both devices on the same network, tap to connect -5. **Present!** — Use the large buttons to control your slides - -## Compatibility - -Works with any presentation software that uses arrow keys: - -- Apple Keynote -- Microsoft PowerPoint -- Google Slides -- Figma -- Canva -- PDF viewers -- And more... - -## Privacy - -Clicker respects your privacy: - -- **No accounts** — No sign-up required -- **No cloud** — All communication is peer-to-peer -- **No analytics** — We don't track your usage -- **No data collection** — Your presentations stay on your devices - -## License - -This project is licensed under the MIT License. See the [LICENSE](https://github.com/douinc/clicker/blob/main/LICENSE) file for details. diff --git a/docs/privacy.md b/docs/privacy.md deleted file mode 100644 index 4e1d4d6..0000000 --- a/docs/privacy.md +++ /dev/null @@ -1,100 +0,0 @@ -# Privacy Policy - -**Last updated: January 22, 2026** - -DOU Inc. ("we", "our", or "us") operates the Clicker application (the "App"). This Privacy Policy describes how we collect, use, and protect your information when you use our App. - -## Summary - -**We don't collect your data.** Clicker is designed to work entirely on your devices without requiring any personal information or data transmission to external servers. - -## Information We Do NOT Collect - -Clicker does not collect, store, or transmit: - -- Personal information (name, email, phone number) -- Location data -- Usage analytics or telemetry -- Presentation content or slide data -- Device identifiers for tracking purposes -- Any data to third-party services - -## How Clicker Works - -### Local Communication Only - -Clicker uses Apple's MultipeerConnectivity framework to establish a direct peer-to-peer connection between your iPhone and Mac. This communication: - -- Occurs entirely over your local network (WiFi) or Bluetooth -- Does not pass through any external servers -- Is not logged or recorded by us -- Contains only simple navigation commands (next slide, previous slide) - -### Subscription Management - -If you subscribe to Clicker on iOS: - -- Subscription purchases are processed entirely by Apple through the App Store -- We do not have access to your payment information -- Apple handles all billing, receipts, and subscription management -- We only receive confirmation that a valid subscription exists (no personal details) - -### Local Storage - -Clicker stores minimal data locally on your device: - -- **Trial start date** — Stored in your device's Keychain to track the 7-day trial period -- **Timer preferences** — Your preferred timer settings (duration, haptic interval) - -This data never leaves your device and is not accessible to us. - -## Permissions - -Clicker requests the following permissions: - -| Permission | Platform | Purpose | -|------------|----------|---------| -| **Local Network** | iOS & macOS | To discover and connect to devices on your network | -| **Accessibility** | macOS only | To send keyboard events to your presentation software | - -These permissions are used solely for the App's core functionality and not for data collection. - -## Children's Privacy - -Clicker does not collect any personal information from anyone, including children under 13. - -## Third-Party Services - -Clicker does not integrate with any third-party analytics, advertising, or tracking services. - -The only third-party service involved is: - -- **Apple App Store** — For processing in-app subscriptions (governed by [Apple's Privacy Policy](https://www.apple.com/legal/privacy/)) - -## Data Security - -Since we don't collect data, there is no data to secure on our end. All communication between your devices is handled by Apple's encrypted MultipeerConnectivity framework. - -## Changes to This Policy - -We may update this Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page and updating the "Last updated" date. - -## Contact Us - -If you have questions about this Privacy Policy, please contact us: - -- **Email**: privacy@dou.inc -- **GitHub**: [github.com/douinc/clicker](https://github.com/douinc/clicker) - -## Your Rights - -Depending on your location, you may have certain rights regarding your personal data. Since Clicker does not collect personal data, these rights are inherently satisfied: - -- **Right to Access** — There is no data to access -- **Right to Deletion** — There is no data to delete -- **Right to Portability** — There is no data to export -- **Right to Opt-Out** — There is no data collection to opt out of - ---- - -This Privacy Policy is effective as of January 22, 2026. diff --git a/docs/reference/keycodes.md b/docs/reference/keycodes.md deleted file mode 100644 index 2259318..0000000 --- a/docs/reference/keycodes.md +++ /dev/null @@ -1,161 +0,0 @@ -# Key Codes Reference - -macOS virtual key codes for use with `CGEvent`. - -## Navigation Keys - -| Key | Code | Common Use | -|-----|------|------------| -| Left Arrow | `123` | Previous slide | -| Right Arrow | `124` | Next slide | -| Up Arrow | `126` | Previous slide (alternative) | -| Down Arrow | `125` | Next slide (alternative) | -| Page Up | `116` | Previous slide | -| Page Down | `121` | Next slide | -| Home | `115` | First slide | -| End | `119` | Last slide | - -## Function Keys - -| Key | Code | Common Use | -|-----|------|------------| -| Escape | `53` | End presentation, exit fullscreen | -| Return | `36` | Start presentation, confirm | -| Space | `49` | Next slide, play/pause | -| Tab | `48` | Next field | -| Delete | `51` | Delete, back | -| F5 | `96` | Start presentation (PowerPoint) | - -## Letter Keys - -| Key | Code | Common Use | -|-----|------|------------| -| B | `11` | Black screen (PowerPoint/Keynote) | -| W | `13` | White screen (PowerPoint/Keynote) | -| P | `35` | Pointer/Pen tool | -| E | `14` | Eraser tool | -| S | `1` | Stop/Start | -| Q | `12` | Quit | - -## Number Keys - -| Key | Code | -|-----|------| -| 1 | `18` | -| 2 | `19` | -| 3 | `20` | -| 4 | `21` | -| 5 | `23` | -| 6 | `22` | -| 7 | `26` | -| 8 | `28` | -| 9 | `25` | -| 0 | `29` | - -## Modifier Keys - -These are used with `CGEventFlags`, not as standalone key codes: - -| Modifier | Flag | Code (if needed) | -|----------|------|------------------| -| Shift | `.maskShift` | `56` (left), `60` (right) | -| Control | `.maskControl` | `59` (left), `62` (right) | -| Option | `.maskAlternate` | `58` (left), `61` (right) | -| Command | `.maskCommand` | `55` (left), `54` (right) | -| Caps Lock | `.maskAlphaShift` | `57` | -| Function | `.maskSecondaryFn` | `63` | - ---- - -## Application-Specific Shortcuts - -### Apple Keynote - -| Action | Key | Code | -|--------|-----|------| -| Start presentation | Return | `36` | -| End presentation | Escape | `53` | -| Next slide | Right Arrow / Space | `124` / `49` | -| Previous slide | Left Arrow | `123` | -| Black screen | B | `11` | -| White screen | W | `13` | -| Show presenter notes | — | Use display settings | - -### Microsoft PowerPoint - -| Action | Key | Code | -|--------|-----|------| -| Start presentation | F5 | `96` | -| Start from current | Shift+F5 | `96` + `.maskShift` | -| End presentation | Escape | `53` | -| Next slide | Right Arrow / Space / N | `124` / `49` / `45` | -| Previous slide | Left Arrow / P | `123` / `35` | -| Black screen | B / Period | `11` / `47` | -| White screen | W / Comma | `13` / `43` | -| Go to slide N | N + Return | Number + `36` | - -### Google Slides - -| Action | Key | Code | -|--------|-----|------| -| Start presentation | Cmd+Return | `36` + `.maskCommand` | -| End presentation | Escape | `53` | -| Next slide | Right Arrow / Space | `124` / `49` | -| Previous slide | Left Arrow | `123` | - ---- - -## Using Key Codes in Clicker - -### Basic Keystroke - -```swift -// In RemoteCommand.swift -case myCommand = "my_command" - -var keyCode: UInt16 { - switch self { - case .myCommand: return 11 // B key - // ... - } -} -``` - -### With Modifiers - -Modify `KeystrokeSender.swift`: - -```swift -func sendKeystroke(_ keyCode: UInt16, modifiers: CGEventFlags = []) { - let source = CGEventSource(stateID: .hidSystemState) - - let keyDown = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: true) - keyDown?.flags = modifiers - keyDown?.post(tap: .cghidEventTap) - - let keyUp = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: false) - keyUp?.post(tap: .cghidEventTap) -} - -// Usage: -sendKeystroke(96, modifiers: .maskShift) // Shift+F5 -``` - ---- - -## Finding Key Codes - -To find the key code for any key: - -1. Use Apple's **Key Codes** app (free on App Store) -2. Or use this Swift snippet: - -```swift -import Cocoa - -NSEvent.addGlobalMonitorForEvents(matching: .keyDown) { event in - print("Key: \(event.characters ?? "") Code: \(event.keyCode)") -} -``` - -Run in a macOS app to see key codes as you type. diff --git a/docs/reference/troubleshooting.md b/docs/reference/troubleshooting.md deleted file mode 100644 index b077184..0000000 --- a/docs/reference/troubleshooting.md +++ /dev/null @@ -1,227 +0,0 @@ -# Troubleshooting - -## Connection Issues - -### Mac not appearing in iPhone app - -**Symptoms**: iPhone shows "Searching..." but no Macs appear. - -**Solutions**: - -1. **Check network**: Both devices must be on the same WiFi network -2. **Check Mac app**: Ensure it's running (look for menu bar icon) -3. **Firewall**: Temporarily disable macOS firewall to test -4. **VPN**: Disconnect any VPN — they often block local traffic -5. **Restart discovery**: Kill and restart both apps - -```mermaid -flowchart TD - A[Mac not appearing?] --> B{Same WiFi?} - B -->|No| C[Connect to same network] - B -->|Yes| D{Mac app running?} - D -->|No| E[Launch Mac app] - D -->|Yes| F{Firewall enabled?} - F -->|Yes| G[Add exception or disable] - F -->|No| H{VPN active?} - H -->|Yes| I[Disconnect VPN] - H -->|No| J[Restart both apps] -``` - -### Connection drops frequently - -**Solutions**: - -1. **WiFi stability**: Move closer to router -2. **Bluetooth fallback**: MultipeerConnectivity will use Bluetooth if WiFi fails -3. **Sleep settings**: Prevent Mac from sleeping during presentation -4. **Power**: Keep iPhone charged or plugged in - -### "Local Network" permission denied - -On iPhone: - -1. Go to **Settings → Privacy & Security → Local Network** -2. Find **Clicker** in the list -3. Enable the toggle - -If Clicker isn't listed, delete and reinstall the app. - ---- - -## Keystroke Issues - -### Keystrokes not working - -**Symptoms**: iPhone shows "Connected" but slides don't change. - -**Solutions**: - -1. **Accessibility permission**: Check System Settings → Privacy & Security → Accessibility -2. **Correct app focused**: The presentation app must be frontmost -3. **Presentation mode**: Some apps only respond to keys in presentation mode - -### Wrong app receiving keystrokes - -`CGEvent` sends keystrokes to the frontmost application. Ensure: - -1. Your presentation app is in front -2. No dialogs or other windows are covering it -3. Menu bar isn't selected - -### Accessibility permission not sticking - -If permission resets after restart: - -1. Remove Clicker from the Accessibility list -2. Delete the app -3. Reinstall and grant permission again - -This can happen if the app was moved or the code signature changed. - ---- - -## Build Issues - -### "Signing requires a development team" - -Add your Team ID to `project.yml`: - -```yaml -settings: - base: - DEVELOPMENT_TEAM: YOUR_TEAM_ID -``` - -Find your Team ID: - -```bash -security find-identity -v -p codesigning | grep "Apple Development" -``` - -Then regenerate: - -```bash -xcodegen generate -``` - -### "Unable to find destination" - -List available destinations: - -```bash -xcodebuild -scheme ClickeriOS -showdestinations -``` - -Use an exact match from the output: - -```bash -xcodebuild -scheme ClickeriOS \ - -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.2' \ - build -``` - -### "No such module" errors - -Clean and rebuild: - -```bash -rm -rf ~/Library/Developer/Xcode/DerivedData/Clicker-* -xcodegen generate -xcodebuild -scheme ClickeriOS build -``` - -### MultipeerConnectivity compile errors - -Ensure `project.yml` has the required Info.plist entries: - -```yaml -targets: - ClickeriOS: - info: - properties: - NSBonjourServices: - - _clicker._tcp - - _clicker._udp - NSLocalNetworkUsageDescription: "..." -``` - ---- - -## Subscription Issues - -### Trial not starting - -The trial starts on first launch. If it doesn't: - -1. Check device date/time is correct -2. Keychain access might be restricted -3. Try deleting and reinstalling the app - -!!! note "Trial Survives Reinstall" - The trial start date is stored in Keychain, which persists across app reinstalls. - -### Purchase not completing - -1. Check internet connection -2. Ensure Apple ID is signed in -3. Try "Restore Purchases" if you've purchased before -4. Check for pending App Store updates - -### Subscription status not updating - -Force refresh: - -1. Kill the app completely -2. Wait 30 seconds -3. Relaunch - -StoreKit caches entitlements; a fresh launch forces a check. - ---- - -## Timer Issues - -### Haptics not working - -1. **Silent mode**: Haptics work even in silent mode, but check iOS settings -2. **Do Not Disturb**: May suppress some feedback -3. **Test button**: Use Settings → Test Haptic to verify - -### Timer inaccurate during background - -iOS may throttle background timers. For best accuracy: - -1. Keep app in foreground -2. Disable auto-lock during presentations -3. Keep screen on (use Guided Access if needed) - ---- - -## General - -### App crashes on launch - -1. Delete and reinstall -2. Check iOS/macOS version meets minimum requirements -3. Report the crash: Settings → Privacy → Analytics → Analytics Data - -### Battery drain - -MultipeerConnectivity uses WiFi and Bluetooth. To minimize drain: - -1. Connect only when presenting -2. Disconnect when done -3. Keep iPhone charged during long presentations - ---- - -## Getting Help - -If you're still stuck: - -1. Check [GitHub Issues](https://github.com/douinc/clicker/issues) for similar problems -2. Open a new issue with: - - Device models and OS versions - - Steps to reproduce - - Any error messages -3. Include logs if possible diff --git a/main.py b/main.py deleted file mode 100644 index a049b2b..0000000 --- a/main.py +++ /dev/null @@ -1,6 +0,0 @@ -def main(): - print("Hello from clicker-docs!") - - -if __name__ == "__main__": - main() diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index ad1f27a..0000000 --- a/mkdocs.yml +++ /dev/null @@ -1,80 +0,0 @@ -site_name: Clicker Documentation -site_url: https://douinc.github.io/clicker -site_description: Developer documentation for Clicker - iPhone presentation remote for Mac -site_author: DOU Inc. - -repo_name: douinc/clicker -repo_url: https://github.com/douinc/clicker - -theme: - name: material - palette: - - scheme: slate - primary: deep purple - accent: orange - toggle: - icon: material/brightness-4 - name: Switch to light mode - - scheme: default - primary: deep purple - accent: orange - toggle: - icon: material/brightness-7 - name: Switch to dark mode - features: - - navigation.instant - - navigation.tabs - - navigation.sections - - navigation.top - - navigation.indexes - - search.highlight - - search.suggest - - content.code.copy - - content.code.annotate - - content.tabs.link - icon: - repo: fontawesome/brands/github - -markdown_extensions: - - admonition - - pymdownx.details - - pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format - - pymdownx.highlight: - anchor_linenums: true - line_spans: __span - pygments_lang_class: true - - pymdownx.inlinehilite - - pymdownx.tabbed: - alternate_style: true - - pymdownx.emoji: - emoji_index: !!python/name:material.extensions.emoji.twemoji - emoji_generator: !!python/name:material.extensions.emoji.to_svg - - attr_list - - md_in_html - - tables - - toc: - permalink: true - -nav: - - Home: index.md - - Getting Started: - - Installation: getting-started/installation.md - - First Run: getting-started/first-run.md - - Development: - - Building from Source: development/building.md - - Project Structure: development/structure.md - - Architecture: development/architecture.md - - Extending: development/extending.md - - Reference: - - Key Codes: reference/keycodes.md - - Troubleshooting: reference/troubleshooting.md - - Privacy Policy: privacy.md - -extra: - social: - - icon: fontawesome/brands/github - link: https://github.com/douinc diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index d5ccdfa..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,9 +0,0 @@ -[project] -name = "clicker-docs" -version = "0.1.0" -description = "Add your description here" -readme = "README.md" -requires-python = ">=3.13" -dependencies = [ - "mkdocs-material>=9.7.1", -] diff --git a/uv.lock b/uv.lock deleted file mode 100644 index 05210d6..0000000 --- a/uv.lock +++ /dev/null @@ -1,452 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.13" - -[[package]] -name = "babel" -version = "2.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, -] - -[[package]] -name = "backrefs" -version = "6.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/86/e3/bb3a439d5cb255c4774724810ad8073830fac9c9dee123555820c1bcc806/backrefs-6.1.tar.gz", hash = "sha256:3bba1749aafe1db9b915f00e0dd166cba613b6f788ffd63060ac3485dc9be231", size = 7011962, upload-time = "2025-11-15T14:52:08.323Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/ee/c216d52f58ea75b5e1841022bbae24438b19834a29b163cb32aa3a2a7c6e/backrefs-6.1-py310-none-any.whl", hash = "sha256:2a2ccb96302337ce61ee4717ceacfbf26ba4efb1d55af86564b8bbaeda39cac1", size = 381059, upload-time = "2025-11-15T14:51:59.758Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9a/8da246d988ded941da96c7ed945d63e94a445637eaad985a0ed88787cb89/backrefs-6.1-py311-none-any.whl", hash = "sha256:e82bba3875ee4430f4de4b6db19429a27275d95a5f3773c57e9e18abc23fd2b7", size = 392854, upload-time = "2025-11-15T14:52:01.194Z" }, - { url = "https://files.pythonhosted.org/packages/37/c9/fd117a6f9300c62bbc33bc337fd2b3c6bfe28b6e9701de336b52d7a797ad/backrefs-6.1-py312-none-any.whl", hash = "sha256:c64698c8d2269343d88947c0735cb4b78745bd3ba590e10313fbf3f78c34da5a", size = 398770, upload-time = "2025-11-15T14:52:02.584Z" }, - { url = "https://files.pythonhosted.org/packages/eb/95/7118e935b0b0bd3f94dfec2d852fd4e4f4f9757bdb49850519acd245cd3a/backrefs-6.1-py313-none-any.whl", hash = "sha256:4c9d3dc1e2e558965202c012304f33d4e0e477e1c103663fd2c3cc9bb18b0d05", size = 400726, upload-time = "2025-11-15T14:52:04.093Z" }, - { url = "https://files.pythonhosted.org/packages/1d/72/6296bad135bfafd3254ae3648cd152980a424bd6fed64a101af00cc7ba31/backrefs-6.1-py314-none-any.whl", hash = "sha256:13eafbc9ccd5222e9c1f0bec563e6d2a6d21514962f11e7fc79872fd56cbc853", size = 412584, upload-time = "2025-11-15T14:52:05.233Z" }, - { url = "https://files.pythonhosted.org/packages/02/e3/a4fa1946722c4c7b063cc25043a12d9ce9b4323777f89643be74cef2993c/backrefs-6.1-py39-none-any.whl", hash = "sha256:a9e99b8a4867852cad177a6430e31b0f6e495d65f8c6c134b68c14c3c95bf4b0", size = 381058, upload-time = "2025-11-15T14:52:06.698Z" }, -] - -[[package]] -name = "certifi" -version = "2026.1.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, - { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, - { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, - { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, - { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, - { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, - { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, - { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, - { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, - { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, - { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, - { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, - { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, - { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, - { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, - { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, - { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, - { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, - { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, - { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, - { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, - { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, - { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, - { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, - { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, - { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, -] - -[[package]] -name = "click" -version = "8.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, -] - -[[package]] -name = "clicker-docs" -version = "0.1.0" -source = { virtual = "." } -dependencies = [ - { name = "mkdocs-material" }, -] - -[package.metadata] -requires-dist = [{ name = "mkdocs-material", specifier = ">=9.7.1" }] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, -] - -[[package]] -name = "ghp-import" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "python-dateutil" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, -] - -[[package]] -name = "idna" -version = "3.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, -] - -[[package]] -name = "jinja2" -version = "3.1.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, -] - -[[package]] -name = "markdown" -version = "3.10.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b7/b1/af95bcae8549f1f3fd70faacb29075826a0d689a27f232e8cee315efa053/markdown-3.10.1.tar.gz", hash = "sha256:1c19c10bd5c14ac948c53d0d762a04e2fa35a6d58a6b7b1e6bfcbe6fefc0001a", size = 365402, upload-time = "2026-01-21T18:09:28.206Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/59/1b/6ef961f543593969d25b2afe57a3564200280528caa9bd1082eecdd7b3bc/markdown-3.10.1-py3-none-any.whl", hash = "sha256:867d788939fe33e4b736426f5b9f651ad0c0ae0ecf89df0ca5d1176c70812fe3", size = 107684, upload-time = "2026-01-21T18:09:27.203Z" }, -] - -[[package]] -name = "markupsafe" -version = "3.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, - { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, - { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, - { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, - { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, - { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, - { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, - { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, - { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, - { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, - { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, - { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, - { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, - { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, - { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, - { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, - { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, - { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, - { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, - { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, - { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, - { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, - { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, - { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, - { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, - { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, - { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, - { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, - { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, - { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, - { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, - { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, -] - -[[package]] -name = "mergedeep" -version = "1.3.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, -] - -[[package]] -name = "mkdocs" -version = "1.6.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "ghp-import" }, - { name = "jinja2" }, - { name = "markdown" }, - { name = "markupsafe" }, - { name = "mergedeep" }, - { name = "mkdocs-get-deps" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "pyyaml" }, - { name = "pyyaml-env-tag" }, - { name = "watchdog" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, -] - -[[package]] -name = "mkdocs-get-deps" -version = "0.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mergedeep" }, - { name = "platformdirs" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, -] - -[[package]] -name = "mkdocs-material" -version = "9.7.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "babel" }, - { name = "backrefs" }, - { name = "colorama" }, - { name = "jinja2" }, - { name = "markdown" }, - { name = "mkdocs" }, - { name = "mkdocs-material-extensions" }, - { name = "paginate" }, - { name = "pygments" }, - { name = "pymdown-extensions" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/27/e2/2ffc356cd72f1473d07c7719d82a8f2cbd261666828614ecb95b12169f41/mkdocs_material-9.7.1.tar.gz", hash = "sha256:89601b8f2c3e6c6ee0a918cc3566cb201d40bf37c3cd3c2067e26fadb8cce2b8", size = 4094392, upload-time = "2025-12-18T09:49:00.308Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/32/ed071cb721aca8c227718cffcf7bd539620e9799bbf2619e90c757bfd030/mkdocs_material-9.7.1-py3-none-any.whl", hash = "sha256:3f6100937d7d731f87f1e3e3b021c97f7239666b9ba1151ab476cabb96c60d5c", size = 9297166, upload-time = "2025-12-18T09:48:56.664Z" }, -] - -[[package]] -name = "mkdocs-material-extensions" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, -] - -[[package]] -name = "packaging" -version = "26.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, -] - -[[package]] -name = "paginate" -version = "0.5.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, -] - -[[package]] -name = "pathspec" -version = "1.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" }, -] - -[[package]] -name = "platformdirs" -version = "4.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, -] - -[[package]] -name = "pygments" -version = "2.19.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, -] - -[[package]] -name = "pymdown-extensions" -version = "10.20" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3e/35/e3814a5b7df295df69d035cfb8aab78b2967cdf11fcfae7faed726b66664/pymdown_extensions-10.20.tar.gz", hash = "sha256:5c73566ab0cf38c6ba084cb7c5ea64a119ae0500cce754ccb682761dfea13a52", size = 852774, upload-time = "2025-12-31T19:59:42.211Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/10/47caf89cbb52e5bb764696fd52a8c591a2f0e851a93270c05a17f36000b5/pymdown_extensions-10.20-py3-none-any.whl", hash = "sha256:ea9e62add865da80a271d00bfa1c0fa085b20d133fb3fc97afdc88e682f60b2f", size = 268733, upload-time = "2025-12-31T19:59:40.652Z" }, -] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, -] - -[[package]] -name = "pyyaml" -version = "6.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, - { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, - { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, - { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, - { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, - { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, - { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, - { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, - { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, -] - -[[package]] -name = "pyyaml-env-tag" -version = "1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, -] - -[[package]] -name = "requests" -version = "2.32.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, -] - -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, -] - -[[package]] -name = "urllib3" -version = "2.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, -] - -[[package]] -name = "watchdog" -version = "6.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, - { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, - { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, - { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, - { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, - { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, - { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, - { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, - { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, - { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, - { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, - { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, -] From 163d61005e199d2bfb45ae5d078aa7ffddc00de2 Mon Sep 17 00:00:00 2001 From: donny-son Date: Sun, 1 Mar 2026 11:22:13 +0900 Subject: [PATCH 4/7] update ai context --- CLAUDE.md | 193 ++++++++++++++---------------------------------------- 1 file changed, 49 insertions(+), 144 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 029744d..42b579e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,89 +2,57 @@ ## Project Overview -This is a SwiftUI-based presentation remote system with two apps: -- **Mac App**: Menu bar app that receives commands and sends keystrokes to presentation software -- **iPhone App**: Remote control with vertical slide navigation and presentation timer +This is a SwiftUI-based presentation remote system with three apps: +- **Mac App** (`ClickerRemoteReceiver` v1.2): Menu bar app that receives commands and sends keystrokes to presentation software +- **iPhone App** (`ClickerRemote` v1.6): Remote control with vertical slide navigation and presentation timer +- **Apple Watch App** (`ClickerWatch` v1.6): Companion watch app with gesture-based slide control using wrist flicks ## Tech Stack - **Language**: Swift 5.9 - **UI Framework**: SwiftUI -- **Networking**: MultipeerConnectivity framework +- **Networking**: MultipeerConnectivity (Mac↔iPhone), WatchConnectivity (iPhone↔Watch) - **Build System**: XcodeGen + xcodebuild (CLI-based, no Xcode GUI required) -- **Platforms**: macOS 14.0+, iOS 18.0+ +- **Platforms**: macOS 14.0+, iOS 18.0+, watchOS 10.0+ - **Design**: Apple liquid glass aesthetic (dark mode, translucent materials) +## Documentation + +Use GitHub wiki as the main source of developer documentation. The documentation is in `./wiki/`. + ## Project Structure ``` clicker/ -├── project.yml # XcodeGen config - defines both targets -├── Shared/ # Code shared between both apps -│ └── RemoteCommand.swift -├── MacApp/ # macOS menu bar app -│ ├── PresentationRemoteMacApp.swift # Main app + SwiftUI views -│ ├── MacConnectionManager.swift # Multipeer session handling -│ ├── KeystrokeSender.swift # CGEvent keystroke injection -│ ├── Info.plist # Base plist (merged by XcodeGen) -│ └── ClickerMac.entitlements +├── MacApp/ # macOS menu bar receiver app ├── iPhoneApp/ # iOS remote control app -│ ├── PresentationRemoteiPhoneApp.swift # Main app + all views -│ ├── iPhoneConnectionManager.swift # Multipeer session handling -│ ├── PresentationTimer.swift # Timer with haptic feedback -│ └── Info.plist # Base plist (merged by XcodeGen) -└── Clicker.xcodeproj/ # Generated - do not edit directly +├── WatchApp/ # Apple Watch companion app +├── Shared/ # Shared code (RemoteCommand.swift) +├── wiki/ # GitHub wiki documentation +├── public/ # Logos and screenshots +├── build/ # Build artifacts +├── .github/ # GitHub Actions workflows +├── project.yml # XcodeGen project configuration +└── justfile # Build and deployment commands ``` ## Build Commands -```bash -# Generate Xcode project from project.yml -xcodegen generate - -# Build Mac app -xcodebuild -scheme ClickerMac build - -# Build iOS app (simulator) -xcodebuild -scheme ClickeriOS -destination 'generic/platform=iOS Simulator' build +Reference @justfile -# Build iOS app (physical device) -xcodebuild -scheme ClickeriOS -destination 'id=XCODE_DEVICE_ID' build - -# Install on iPhone -xcrun devicectl device install app --device XCODE_DEVICE_ID ~/Library/Developer/Xcode/DerivedData/Clicker-*/Build/Products/Debug-iphoneos/ClickerRemote.app +## Distribution -# Launch on iPhone -xcrun devicectl device process launch --device DEVICECTL_ID com.dou.clicker-ios +### iOS and Apple Watch App (App Store) -# Run Mac app -open ~/Library/Developer/Xcode/DerivedData/Clicker-*/Build/Products/Debug/ClickerRemoteReceiver.app -``` +The iOS and companion Apple Watch app is distributed via the App Store. -## Distribution - -### iOS App (App Store) -The iOS app is distributed via the App Store. Use the following commands: -```bash -make release-ios # Archive and upload to App Store Connect -``` Then go to App Store Connect to select the build for TestFlight/review. ### Mac App (GitHub DMG - Signed & Notarized) -The Mac app is distributed as a signed and notarized DMG via GitHub Releases. -**One-time setup:** -1. Create a Developer ID Application certificate at [Apple Developer Portal](https://developer.apple.com/account/resources/certificates/list) -2. Create an app-specific password at [appleid.apple.com](https://appleid.apple.com) -3. Run `make setup-notary` to store credentials in keychain - -**Release workflow:** -```bash -make check-signing # Verify Developer ID certificate is installed -make release-mac # Build, sign, notarize, and create DMG -``` +The Mac app is distributed as a signed and notarized DMG via GitHub Releases. -This runs through the full pipeline: +The full pipeline: 1. Build Release configuration with Hardened Runtime 2. Create DMG with custom icon 3. Sign DMG with Developer ID @@ -93,20 +61,10 @@ This runs through the full pipeline: The notarized DMG is created at `./build/ClickerRemoteReceiver-{version}.dmg`. -**Individual commands:** -```bash -make dmg # Create unsigned DMG -make sign-dmg # Sign DMG with Developer ID -make notarize # Submit for notarization and staple -make verify-signing # Verify app signature -make verify-notarization # Verify DMG is notarized -make notary-log # Show recent notarization submissions -``` - **Create GitHub release and update Homebrew tap:** ```bash # Create the release -gh release create v1.1 ./build/ClickerRemoteReceiver-1.1.dmg --title 'Clicker v1.1' --notes 'Release notes' +gh release create v1.2 ./build/ClickerRemoteReceiver-1.2.dmg --title 'Clicker v1.2' --notes 'Release notes' # Trigger homebrew-tap update (auto-calculates SHA256) just update-tap @@ -121,74 +79,46 @@ The `update-tap` command triggers a GitHub Action in `douinc/homebrew-tap` that: ## Key Architecture Decisions ### XcodeGen Configuration -- `project.yml` defines both targets in a single unified project +- `project.yml` defines all three targets (Mac, iOS, Watch) in a single unified project - Info.plist keys are specified in `info.properties` section (not just `info.path`) - This is critical for Multipeer Connectivity which requires `NSBonjourServices` and `NSLocalNetworkUsageDescription` +- The Watch app is embedded in the iOS target via `dependencies` -### Multipeer Connectivity -- Service type: `_clicker._tcp` and `_clicker._udp` +### Multipeer Connectivity (Mac↔iPhone) +- Service type: `_clickerremote._tcp` and `_clickerremote._udp` - Mac acts as advertiser, iPhone acts as browser - Commands are sent as JSON-encoded `RemoteCommand` enum values +- Shared config in `Shared/RemoteCommand.swift` + +### WatchConnectivity (iPhone↔Watch) +- iPhone relays Watch commands to Mac via MultipeerConnectivity +- Watch sends commands using `WCSession.default.sendMessage` ### Mac App Specifics - App name: `ClickerRemoteReceiver` (distributed via GitHub DMG) +- Bundle ID: `com.dou.clicker-mac` - `LSUIElement: true` makes it a menu bar app (no Dock icon) - Requires Accessibility permission for CGEvent keystroke injection - Uses `CGEvent` API to send keyboard events to frontmost app ### iPhone App Specifics - App name: `ClickerRemote` (distributed via App Store) +- Bundle ID: `com.dou.clicker-ios` - Vertical button layout: Previous (chevron up) at top, Next (chevron down) at bottom - Liquid glass aesthetic using `.ultraThinMaterial` for frosted glass effect - Dark mode only (`.preferredColorScheme(.dark)`) for stage visibility - Timer uses `UIImpactFeedbackGenerator` for haptic feedback -- Uses modern SwiftUI with `presentationDetents` for sheet sizing - -## Common Issues & Solutions - -### Notarization fails with "Invalid" status -Check the notarization log with `xcrun notarytool log --keychain-profile notarytool-profile`. Common causes: - -1. **Wrong certificate**: Using "Apple Distribution" instead of "Developer ID Application" -2. **Missing timestamp**: Signature needs `--timestamp` flag -3. **Debug entitlement**: `com.apple.security.get-task-allow` is forbidden - -The fix requires these Release-only settings in `project.yml`: -```yaml -configs: - Release: - CODE_SIGN_STYLE: Manual - CODE_SIGN_IDENTITY: "Developer ID Application" - CODE_SIGN_INJECT_BASE_ENTITLEMENTS: NO - OTHER_CODE_SIGN_FLAGS: "--timestamp" -``` - -### "Signing requires development team" -Add `DEVELOPMENT_TEAM: YOUR_TEAM_ID` to `project.yml` under the target's settings, then run `xcodegen generate`. - -### "Unable to find destination" -Specify OS version: `-destination 'platform=iOS Simulator,name=iPhone 16,OS=18.2'` - -### Multipeer not connecting -Ensure `NSBonjourServices` and `NSLocalNetworkUsageDescription` are in `project.yml` under `info.properties`, not just in the source Info.plist files. - -### SwiftUI Section syntax error -When using `Section` with both header and footer, use: -```swift -Section { - // content -} header: { - Text("Header") -} footer: { - Text("Footer") -} -``` -NOT: `Section("Header") { ... } footer: { ... }` - -## Bundle Identifiers - -- Mac (ClickerRemoteReceiver): `com.dou.clicker-mac` -- iOS (ClickerRemote): `com.dou.clicker-ios` +- In-App Purchase capability for subscription/trial + +### Apple Watch App Specifics +- App name: `ClickerWatch` (embedded in iOS app, distributed via App Store) +- Bundle ID: `com.dou.clicker-ios.watchkitapp` +- Gesture detection via CoreMotion (wrist flick rotation rate on x-axis) +- Gesture lock: 3-second lockout after gesture to prevent accidental triggers +- Gesture inversion: option to swap flick direction mapping +- Auto-toggle: gesture activation follows wrist raise/lower +- HealthKit workout session keeps app active during presentations +- Extended WatchKit session for background operation ## Development Team @@ -198,32 +128,7 @@ Team ID: `HD35YQ72U4` (DOU Inc.) 1. Edit Swift source files directly 2. If changing build settings, targets, or Info.plist keys, edit `project.yml` -3. After editing `project.yml`, always run `xcodegen generate` -4. Do NOT edit `Clicker.xcodeproj` directly - it's generated - -## Makefile Commands - -```bash -make help # Show all available commands -make generate # Generate Xcode project from project.yml -make build-mac # Build Mac app (debug) -make build-ios # Build iOS app for device -make build-sim # Build iOS app for simulator -make run-mac # Build and run Mac app -make run-ios # Build, install, and launch on device - -# Mac Distribution (Signed + Notarized) -make check-signing # Verify Developer ID certificate exists -make setup-notary # Store notarization credentials (one-time) -make release-mac # Full pipeline: build, sign, notarize, DMG -make verify-signing # Verify app code signature -make verify-notarization # Verify DMG is notarized -make notary-log # Show recent notarization submissions -make update-tap # Trigger homebrew-tap update after GitHub release - -# iOS Distribution (App Store) -make release-ios # Archive and upload iOS to App Store Connect -``` +3. Run `just generate` after modifying `project.yml` to regenerate the Xcode project ## Useful Debugging Commands From 8693d82fe17277a54f74b567567aabc9cac41f20 Mon Sep 17 00:00:00 2001 From: donny-son Date: Sun, 1 Mar 2026 11:51:12 +0900 Subject: [PATCH 5/7] v1.6: update documentation --- README.md | 8 ++-- index.html | 2 +- project.yml | 4 +- wiki/API-Reference.md | 47 +++++++++++++++++++---- wiki/Architecture.md | 18 ++++++--- wiki/Building.md | 85 +++++++++++++++++++++-------------------- wiki/Contributing.md | 6 +-- wiki/Getting-Started.md | 15 +++++--- wiki/Home.md | 10 ++--- wiki/Troubleshooting.md | 8 ++-- 10 files changed, 125 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index aea5636..0daca34 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@

App Store Download DMG - Platform + Platform License

@@ -103,7 +103,7 @@ graph LR end W -->|WatchConnectivity| A - A -->|WiFi / Bluetooth| B + A -->|MultipeerConnectivity| B B -->|Keystrokes| C ``` @@ -212,9 +212,11 @@ xcodebuild -scheme ClickerMac -configuration Release build ``` clicker/ ├── project.yml # XcodeGen configuration +├── justfile # Build automation ├── Shared/ # Shared code (RemoteCommand) ├── MacApp/ # macOS menu bar app -└── iPhoneApp/ # iOS remote control app +├── iPhoneApp/ # iOS remote control app +└── WatchApp/ # watchOS companion app ``` ### Architecture diff --git a/index.html b/index.html index 669003d..b17ccae 100644 --- a/index.html +++ b/index.html @@ -81,7 +81,7 @@

CLICKER REMOTE

-

$ brew tap douinc/clicker https://github.com/douinc/clicker

+

$ brew tap douinc/tap

$ brew install --cask clicker-remote-receiver

> Ready to present! 🎉

diff --git a/project.yml b/project.yml index 0b1839d..efa927f 100644 --- a/project.yml +++ b/project.yml @@ -94,7 +94,7 @@ targets: PRODUCT_BUNDLE_IDENTIFIER: com.dou.clicker-ios PRODUCT_NAME: ClickerRemote IPHONEOS_DEPLOYMENT_TARGET: "18.0" - MARKETING_VERSION: "1.5" + MARKETING_VERSION: "1.6" CURRENT_PROJECT_VERSION: "1" CODE_SIGN_STYLE: Automatic DEVELOPMENT_TEAM: HD35YQ72U4 @@ -139,7 +139,7 @@ targets: PRODUCT_BUNDLE_IDENTIFIER: com.dou.clicker-ios.watchkitapp PRODUCT_NAME: ClickerWatch WATCHOS_DEPLOYMENT_TARGET: "10.0" - MARKETING_VERSION: "1.5" + MARKETING_VERSION: "1.6" CURRENT_PROJECT_VERSION: "1" CODE_SIGN_STYLE: Automatic DEVELOPMENT_TEAM: HD35YQ72U4 diff --git a/wiki/API-Reference.md b/wiki/API-Reference.md index 8dc43cb..acc4446 100644 --- a/wiki/API-Reference.md +++ b/wiki/API-Reference.md @@ -11,8 +11,14 @@ The command protocol between iPhone and Mac: ```swift // Shared/RemoteCommand.swift enum RemoteCommand: String, Codable { - case next = "next" - case previous = "previous" + case nextSlide = "next" + case previousSlide = "previous" + case startPresentation = "start" + case endPresentation = "end" + case blackScreen = "black" + case keepalive = "keepalive" + + var keyCode: UInt16? { ... } } ``` @@ -54,8 +60,11 @@ Injects keyboard events into the system. class KeystrokeSender { static let shared: KeystrokeSender - func sendNext() // Sends Down Arrow (key code 125) - func sendPrevious() // Sends Up Arrow (key code 126) + func sendNext() // Sends Right Arrow (key code 124) + func sendPrevious() // Sends Left Arrow (key code 123) + func sendStart() // Sends Return (key code 36) + func sendEnd() // Sends Escape (key code 53) + func sendBlackScreen() // Sends B key (key code 11) } ``` @@ -65,8 +74,11 @@ class KeystrokeSender { | Action | Key Code | Key | |--------|----------|-----| -| Next Slide | 125 | ↓ Down Arrow | -| Previous Slide | 126 | ↑ Up Arrow | +| Next Slide | 124 | → Right Arrow | +| Previous Slide | 123 | ← Left Arrow | +| Start Presentation | 36 | Return | +| End Presentation | 53 | Escape | +| Black Screen | 11 | B key | --- @@ -228,11 +240,32 @@ class WatchConnectionManager: NSObject, ObservableObject { **Retry Logic**: Commands are retried up to 3 times at 0.5s intervals if the iPhone is temporarily unreachable. +### GestureManager + +Handles CoreMotion wrist gesture detection for hands-free slide control. + +```swift +class GestureManager: ObservableObject { + @Published var isGestureEnabled: Bool + @Published var gestureLockEnabled: Bool + @Published var isInverted: Bool + @Published var autoToggleWithWrist: Bool +} +``` + +| Property | Type | Description | +|----------|------|-------------| +| `isGestureEnabled` | `Bool` | Whether gesture detection is active | +| `gestureLockEnabled` | `Bool` | 3-second lockout after gesture to prevent accidental triggers | +| `isInverted` | `Bool` | Swap flick direction mapping | +| `autoToggleWithWrist` | `Bool` | Gestures enable on wrist raise, disable on wrist lower | + ### Watch SwiftUI Views | View | Description | |------|-------------| -| `ContentView` | Previous/next buttons + timer display | +| `ContentView` | Previous/next buttons + timer display + gesture toggle | +| `SettingsView` | Gesture lock, inversion, and auto-toggle settings | **Timer Gestures**: - **Tap**: Start/stop timer diff --git a/wiki/Architecture.md b/wiki/Architecture.md index 153b8eb..73866f2 100644 --- a/wiki/Architecture.md +++ b/wiki/Architecture.md @@ -71,11 +71,13 @@ Commands are JSON-encoded `RemoteCommand` values: ```swift // Shared/RemoteCommand.swift enum RemoteCommand: String, Codable { - case next = "next" - case previous = "previous" + case nextSlide = "next" + case previousSlide = "previous" + case startPresentation = "start" + case endPresentation = "end" + case blackScreen = "black" + case keepalive = "keepalive" } - -// Wire format: {"rawValue": "next"} ``` ### Connection Sequence @@ -153,8 +155,12 @@ func sendKeystroke(_ keyCode: UInt16) { | Command | Key Code | Key | |---------|----------|-----| -| Next | 125 | ↓ (Down Arrow) | -| Previous | 126 | ↑ (Up Arrow) | +| Next Slide | 124 | → (Right Arrow) | +| Previous Slide | 123 | ← (Left Arrow) | +| Start Presentation | 36 | Return | +| End Presentation | 53 | Escape | +| Black Screen | 11 | B key | +| Keepalive | — | No keystroke | ## Apple Watch Architecture diff --git a/wiki/Building.md b/wiki/Building.md index 53029d2..cee054d 100644 --- a/wiki/Building.md +++ b/wiki/Building.md @@ -16,21 +16,21 @@ xcodegen generate ```bash # Build -make build-mac +just build-mac # Build and run -make run-mac +just run-mac ``` ### iOS App (Debug) ```bash # Simulator -make build-sim +just build-sim -# Physical device -make build-ios -make run-ios DEVICE_ID= +# Physical device (set XCODE_DEVICE_ID and DEVICECTL_ID in .env) +just build-ios +just run-ios ``` ## Distribution Builds @@ -41,7 +41,7 @@ The iOS app is distributed via App Store Connect: ```bash # Archive and upload to App Store Connect -make release-ios +just release-ios ``` This creates an archive and uploads to App Store Connect. Then: @@ -65,7 +65,7 @@ The Mac app is distributed as a signed and notarized DMG. 3. **Store Notarization Credentials** ```bash - make setup-notary + just setup-notary # Enter your Apple ID and app-specific password ``` @@ -73,10 +73,10 @@ The Mac app is distributed as a signed and notarized DMG. ```bash # Verify signing setup -make check-signing +just check-signing # Full release: build, sign, notarize, create DMG -make release-mac +just release-mac ``` The pipeline: @@ -91,12 +91,12 @@ Output: `./build/ClickerRemoteReceiver-{version}.dmg` #### Individual Commands ```bash -make dmg # Create unsigned DMG -make sign-dmg # Sign DMG -make notarize # Submit for notarization -make verify-signing # Verify app signature -make verify-notarization # Verify DMG is notarized -make notary-log # Show notarization history +just dmg # Create unsigned DMG +just sign-dmg # Sign DMG +just notarize # Submit for notarization +just verify-signing # Verify app signature +just verify-notarization # Verify DMG is notarized +just notary-log # Show notarization history ``` #### Create GitHub Release @@ -110,22 +110,22 @@ gh release create v1.0 \ ## Homebrew Distribution -The Homebrew cask formula is in `Casks/clicker-remote-receiver.rb`. +The Homebrew cask is hosted in the [douinc/homebrew-tap](https://github.com/douinc/homebrew-tap) repository. ### Update Cask for New Release -1. Update `version` in the cask formula -2. Calculate SHA256 of the new DMG: - ```bash - shasum -a 256 ./build/ClickerRemoteReceiver-{version}.dmg - ``` -3. Update `sha256` in the formula -4. Commit and push +After creating a GitHub release, trigger the automated tap update: + +```bash +just update-tap +``` + +This triggers a GitHub Action in `douinc/homebrew-tap` that automatically downloads the DMG, calculates SHA256, updates the Cask formula, and pushes. ### Install from Tap ```bash -brew tap douinc/clicker https://github.com/douinc/clicker +brew tap douinc/tap brew install --cask clicker-remote-receiver ``` @@ -156,29 +156,30 @@ targets: | `CODE_SIGN_INJECT_BASE_ENTITLEMENTS` | Prevent debug entitlements | | `--timestamp` | Required for notarization verification | -## Makefile Reference +## justfile Reference ```bash -make help # Show all commands +just # Show all commands # Development -make generate # Generate Xcode project -make build-mac # Build Mac (debug) -make build-ios # Build iOS for device -make build-sim # Build iOS for simulator -make run-mac # Build and run Mac -make run-ios # Build, install, launch iOS +just generate # Generate Xcode project +just build-mac # Build Mac (debug) +just build-ios # Build iOS for device +just build-sim # Build iOS for simulator +just run-mac # Build and run Mac +just run-ios # Build, install, launch iOS on device +just deploy # Deploy iPhone + Watch to device # Mac Distribution -make check-signing # Verify Developer ID cert -make setup-notary # Store notarization creds -make release-mac # Full release pipeline -make dmg # Create DMG only -make sign-dmg # Sign DMG -make notarize # Notarize DMG -make verify-signing # Verify signature -make verify-notarization # Verify notarization +just check-signing # Verify Developer ID cert +just setup-notary # Store notarization creds +just release-mac # Full release pipeline +just dmg # Create DMG only +just sign-dmg # Sign DMG +just notarize # Notarize DMG +just verify-signing # Verify signature +just verify-notarization # Verify notarization # iOS Distribution -make release-ios # Archive and upload to ASC +just release-ios # Archive and upload to ASC ``` diff --git a/wiki/Contributing.md b/wiki/Contributing.md index 9db1fa1..0edb756 100644 --- a/wiki/Contributing.md +++ b/wiki/Contributing.md @@ -33,11 +33,11 @@ git checkout -b fix/bug-description ```bash # Build both apps -make build-mac -make build-sim +just build-mac +just build-sim # Run and test manually -make run-mac +just run-mac ``` ### Commit Guidelines diff --git a/wiki/Getting-Started.md b/wiki/Getting-Started.md index d14e9d2..ee9c8d5 100644 --- a/wiki/Getting-Started.md +++ b/wiki/Getting-Started.md @@ -29,13 +29,13 @@ open Clicker.xcodeproj ```bash # Debug build -make build-mac +just build-mac # Or directly with xcodebuild xcodebuild -scheme ClickerMac -configuration Debug build # Run the app -make run-mac +just run-mac ``` The Mac app will appear in your menu bar. @@ -45,7 +45,7 @@ The Mac app will appear in your menu bar. ### Simulator ```bash -make build-sim +just build-sim # Or with specific simulator xcodebuild -scheme ClickeriOS \ @@ -60,9 +60,14 @@ xcodebuild -scheme ClickeriOS \ ```bash xcrun devicectl list devices ``` -3. Build and install: +3. Set device IDs in `.env` file: ```bash - make run-ios DEVICE_ID= + XCODE_DEVICE_ID= + DEVICECTL_ID= + ``` +4. Build and install: + ```bash + just run-ios ``` ## Development Team Setup diff --git a/wiki/Home.md b/wiki/Home.md index d7c4737..1aff0d1 100644 --- a/wiki/Home.md +++ b/wiki/Home.md @@ -61,15 +61,15 @@ graph LR ``` clicker/ ├── project.yml # XcodeGen configuration -├── Makefile # Build automation +├── justfile # Build automation ├── Shared/ # Shared code between apps │ └── RemoteCommand.swift ├── MacApp/ # macOS menu bar app ├── iPhoneApp/ # iOS remote control app ├── WatchApp/ # watchOS companion app -├── docs/ # MkDocs documentation ├── wiki/ # GitHub Wiki source -└── Casks/ # Homebrew cask formula +├── public/ # Logos and screenshots +└── .github/ # GitHub Actions workflows ``` ## Distribution @@ -77,8 +77,8 @@ clicker/ | App | Distribution | Link | |-----|--------------|------| | ClickerRemoteReceiver | GitHub Releases (DMG) | [Releases](https://github.com/douinc/clicker/releases) | -| ClickerRemoteReceiver | Homebrew Cask | `brew tap douinc/clicker && brew install --cask clicker-remote-receiver` | -| ClickerRemote | App Store | [Coming Soon](https://apps.apple.com) | +| ClickerRemoteReceiver | Homebrew Cask | `brew tap douinc/tap && brew install --cask clicker-remote-receiver` | +| ClickerRemote | App Store | [App Store](https://apps.apple.com/us/app/clickerremote/id6758130180) | | ClickerWatch | Bundled with iOS app | Installs automatically via Watch app | ## License diff --git a/wiki/Troubleshooting.md b/wiki/Troubleshooting.md index 6cd0a12..c9ff5b1 100644 --- a/wiki/Troubleshooting.md +++ b/wiki/Troubleshooting.md @@ -46,7 +46,7 @@ Common issues and their solutions. 2. **Check Frontmost App**: Ensure your presentation app (Keynote, PowerPoint) is in focus -3. **Test with TextEdit**: Open TextEdit and try — you should see up/down arrow behavior +3. **Test with TextEdit**: Open TextEdit and try — you should see left/right arrow behavior ### Wrong Key Actions @@ -55,8 +55,8 @@ Common issues and their solutions. **Solutions**: - Different presentation apps may use different keys -- ClickerRemote sends Up/Down arrow keys -- Most apps: Down = Next, Up = Previous +- ClickerRemote sends Right/Left arrow keys +- Most apps: Right = Next, Left = Previous - Some apps may be configured differently ## Build Issues @@ -79,7 +79,7 @@ security find-identity -v -p codesigning **Check the log**: ```bash -make notary-log +just notary-log # Then view specific submission: xcrun notarytool log --keychain-profile notarytool-profile ``` From 693550b5391fe2c120cf5b0c33490c6a18d8dbff Mon Sep 17 00:00:00 2001 From: donny-son Date: Sun, 1 Mar 2026 12:28:36 +0900 Subject: [PATCH 6/7] update webpage --- PRIVACY_POLICY.html | 180 +++++++++++++---- TERMS_OF_SERVICE.html | 206 +++++++++++++------ index.html | 447 +++++++++++++++++++++++++++++------------- 3 files changed, 606 insertions(+), 227 deletions(-) diff --git a/PRIVACY_POLICY.html b/PRIVACY_POLICY.html index ccc7465..72fef8d 100644 --- a/PRIVACY_POLICY.html +++ b/PRIVACY_POLICY.html @@ -2,44 +2,156 @@ - + Privacy Policy - ClickerRemote + + -
- ← BACK -

PRIVACY POLICY

-

Last updated: January 31, 2026

-

[ OVERVIEW ]

-

ClickerRemote and ClickerRemoteReceiver are designed with privacy as a core principle. We do not collect, store, or transmit any personal data to external servers.

-

[ DATA COLLECTION ]

-

We do NOT collect:

-
  • Personal information
  • Usage analytics
  • Device identifiers
  • Location data
  • Presentation content
-

[ LOCAL NETWORK ]

-

The apps communicate directly between your iPhone and Mac over your local network:

-
  • Uses peer-to-peer technology
  • Never routes through external servers
  • Stays within your local network
-

[ PERMISSIONS ]

-

iOS App:

  • Local Network - Required to discover and connect to Mac
-

Mac App:

  • Accessibility - Required to simulate keyboard input
  • Local Network - Required to receive connections
-

[ CONTACT ]

-

Questions? team@dou.so

-

Source: github.com/douinc/clicker

-
+
+ + +
+

Privacy Policy

+

Last updated: January 31, 2026

+ +

Overview

+

ClickerRemote and ClickerRemoteReceiver are designed with privacy as a core principle. We do not collect, store, or transmit any personal data to external servers.

+ +

Data Collection

+

We do NOT collect:

+
    +
  • Personal information
  • +
  • Usage analytics
  • +
  • Device identifiers
  • +
  • Location data
  • +
  • Presentation content
  • +
+ +

Local Network

+

The apps communicate directly between your iPhone and Mac over your local network:

+
    +
  • Uses peer-to-peer technology
  • +
  • Never routes through external servers
  • +
  • Stays within your local network
  • +
+ +

Permissions

+

iOS App:

+
    +
  • Local Network — Required to discover and connect to Mac
  • +
+

Mac App:

+
    +
  • Accessibility — Required to simulate keyboard input
  • +
  • Local Network — Required to receive connections
  • +
+ +

Contact

+

Questions? team@dou.so

+

Source: github.com/douinc/clicker

+
+ + +
diff --git a/TERMS_OF_SERVICE.html b/TERMS_OF_SERVICE.html index ac76530..625b251 100644 --- a/TERMS_OF_SERVICE.html +++ b/TERMS_OF_SERVICE.html @@ -2,68 +2,158 @@ - + Terms of Service - ClickerRemote + + -
- ← BACK -

TERMS OF SERVICE

-

Last updated: January 31, 2026

- -

[ AGREEMENT ]

-

By using Clicker, you agree to these Terms. If you don't agree, please don't use the App.

- -

[ THE SERVICE ]

-

Clicker lets you control presentations on your Mac using your iPhone via local network communication.

- -

[ LICENSE ]

-

We grant you a limited, non-exclusive license to use the App. You may NOT:

-
    -
  • Reverse engineer or decompile the App
  • -
  • Use the App for unlawful purposes
  • -
  • Redistribute or resell the App
  • -
- -

[ SUBSCRIPTIONS ]

-
    -
  • 7-day free trial included
  • -
  • Processed through Apple's App Store
  • -
  • Auto-renews unless cancelled 24h before period ends
  • -
  • Manage in Apple ID Account Settings
  • -
- -

[ DISCLAIMERS ]

-

The App is provided "AS IS" without warranties. We don't guarantee error-free operation or compatibility with all software.

- -

[ LIABILITY ]

-

DOU Inc. is not liable for indirect, incidental, or consequential damages. Maximum liability is limited to amounts paid in the preceding 12 months.

- -

[ CHANGES ]

-

We may modify these Terms at any time. Continued use constitutes acceptance.

- -

[ GOVERNING LAW ]

-

These Terms are governed by the laws of the Republic of Korea.

- -

[ CONTACT ]

-

Questions? team@dou.so

-

Source: github.com/douinc/clicker

-
+
+ + +
+

Terms of Service

+

Last updated: January 31, 2026

+ +

Agreement

+

By using Clicker, you agree to these Terms. If you don't agree, please don't use the App.

+ +

The Service

+

Clicker lets you control presentations on your Mac using your iPhone via local network communication.

+ +

License

+

We grant you a limited, non-exclusive license to use the App. You may NOT:

+
    +
  • Reverse engineer or decompile the App
  • +
  • Use the App for unlawful purposes
  • +
  • Redistribute or resell the App
  • +
+ +

Subscriptions

+
    +
  • 7-day free trial included
  • +
  • Processed through Apple's App Store
  • +
  • Auto-renews unless cancelled 24h before period ends
  • +
  • Manage in Apple ID Account Settings
  • +
+ +

Disclaimers

+

The App is provided "AS IS" without warranties. We don't guarantee error-free operation or compatibility with all software.

+ +

Liability

+

DOU Inc. is not liable for indirect, incidental, or consequential damages. Maximum liability is limited to amounts paid in the preceding 12 months.

+ +

Changes

+

We may modify these Terms at any time. Continued use constitutes acceptance.

+ +

Governing Law

+

These Terms are governed by the laws of the Republic of Korea.

+ +

Contact

+

Questions? team@dou.so

+

Source: github.com/douinc/clicker

+
+ + +
diff --git a/index.html b/index.html index b17ccae..51cfb5f 100644 --- a/index.html +++ b/index.html @@ -2,149 +2,326 @@ - - ClickerRemote - Control Presentations from Your iPhone - - - - - - + + ClickerRemote — Liquid Glass Presentation Remote + + + + + -
-
-
- - -
-

CLICKER REMOTE

-

Control presentations from your iPhone

-

No dongles. No cloud. No BS.

-
- -
-
- - - -
-
-

$ brew tap douinc/tap

-

$ brew install --cask clicker-remote-receiver

-

> Ready to present! 🎉

-
-
- -
-

[ DEMO ]

- -
+
+ + +
+
+
SwiftUI • MultipeerConnectivity • WatchConnectivity
+

Presentation Remote for Apple Fans

+

+ Control Keynote, PowerPoint, or Google Slides without dongles or cloud accounts. ClickerRemote pairs your Mac, + iPhone, and Apple Watch using encrypted local networking so every tap lands exactly when you need it. +

+ +
+
Mac 1.2ClickerRemoteReceiver
+
iOS 1.6ClickerRemote
+
watchOS 1.6ClickerWatch
+
+
+
+
+ +
+
+
+ +
+
+

Designed for stage control, not slide anxiety

+

Swift 5.9, SwiftUI, and Apple’s haptics toolkit combine for a tactile experience that mirrors the clickers pros carry on stage. Every surface follows the liquid glass aesthetic outlined in our product design language.

+
+
+
+ 01 +

Peer-to-peer reliability

+

No external servers, accounts, or analytics. The Mac advertises Bonjour services and the iPhone joins instantly on the same network.

+
+
+ 02 +

Cross-device gesture flow

+

Apple Watch flick gestures trigger the same RemoteCommand used by the iPhone UI, routed through WatchConnectivity with built-in lockout guards.

+
+
+ 03 +

Timer with haptic cues

+

Set presentation timers and feel pulses as milestones approach—ideal for keynotes with tight run-of-show schedules.

+
+
+ 04 +

Menu bar minimalism

+

The macOS receiver lives in the menu bar, requests accessibility permissions once, and injects previous and next keystrokes for any frontmost app.

+
+
+
+ +
+
+

One system, three tailored experiences

+

Each binary ships with the right distribution channel: App Store for iPhone + Watch, notarized DMG for Mac. Shared Swift packages keep RemoteCommand and networking logic aligned.

+
+
+
+ iOS logo +

ClickerRemote (iPhone)

+
    +
  • Liquid glass vertical remote layout
  • +
  • Dark-only UI for backstage visibility
  • +
  • Subscription-ready with trials
  • +
  • Automatic Mac discovery via MultipeerConnectivity
  • +
+
+
+ macOS logo +

ClickerRemoteReceiver (Mac)

+
    +
  • Signed + notarized DMG workflow
  • +
  • Menu bar controller and keystroke bridge
  • +
  • Hardened Runtime, accessibility permissions
  • +
  • DMG distributed on GitHub with Homebrew tap
  • +
+
+
+ Apple Watch screenshot +

ClickerWatch (Apple Watch)

+
    +
  • Installed automatically with iOS app
  • +
  • Gesture-based gestures with inversion option
  • +
  • Auto-toggle tied to wrist raise/lower
  • +
  • Workout session keeps app alive
  • +
  • Relays commands through iPhone instantly
  • +
+
+
+
+ +
+
+

Simple onboarding before showtime

+

Install the Mac App and iOS app. Test the iOS or Watch app. Connect your Mac to the screen.

+
+
+
+

Install the Mac receiver

+

Download the notarized DMG, drag `ClickerRemoteReceiver` to Applications, and grant accessibility control when prompted.

+
+
+

Pair the iPhone remote

+

Install from the App Store, open ClickerRemote, and select your Mac from the Bonjour list. No manual IP addresses required.

+
+
+

Activate the Watch

+

Enable the Watch companion from the iOS app. Wrist flicks become precise previous/next commands with a 3-second lockout.

+
+
+

Present with confidence

+

Swipe or tap to advance slides while the timer, haptics, and liquid glass UI keep you focused on your story.

+
+
+
+ +
+
+

Install via Homebrew

+

Get the Mac receiver up and running in seconds with Homebrew. No manual downloads or drag-and-drop required.

+
+
+
$ brew tap douinc/clicker https://github.com/douinc/clicker
+
$ brew install --cask clicker-remote-receiver
+
+
+ +
+
+

Architecture snapshot

+

Every layer is documented in the GitHub wiki and compiled via XcodeGen for reproducible builds.

+
+
+
+

Stack

+
    +
  • Language: Swift 5.9 with shared RemoteCommand models
  • +
  • UI: SwiftUI
  • +
  • Networking: MultipeerConnectivity + WatchConnectivity
  • +
  • Build: XcodeGen + `just` automation
  • +
  • Platforms: macOS 14+, iOS 18+, watchOS 10+
  • +
+
+
+

Distribution

+
    +
  • iOS & Watch: App Store via TestFlight pipeline
  • +
  • Mac: Signed + notarized DMG
  • +
  • Homebrew tap: Easily download and update ClickerRemoteReceiver Mac app using homebrew
  • +
+
+
+

Security posture

+
    +
  • Local only: Commands stay on your network
  • +
  • Accessibility: Minimal permissions, once per Mac
  • +
  • Hardened Runtime: Required for notarization
  • +
  • Watch lockout: Prevents accidental slide jumps
  • +
+
+
+
+ +
+
+

Need help or ready to contribute?

+

Everything—Swift code, wiki docs, and distribution scripts—lives on GitHub. File issues, follow TestFlight betas, or dive into the wiki if you want to extend Clicker to your stage setup.

+
+ +
-
-
- 📱 -

PEER-TO-PEER

-

Direct connection between iPhone and Mac. No servers, no latency.

-
-
- -

APPLE WATCH

-

Control slides with tap buttons or hands-free wrist gestures. Haptic confirmation on every action.

-
-
- 🔒 -

PRIVATE

-

No accounts required. Your data stays on your devices.

-
-
- ☀️ -

ALWAYS-ON

-

Screen stays on while presenting. No connection drops mid-talk.

-
-
- -

UNIVERSAL

-

Works with Keynote, PowerPoint, Google Slides, Figma & more.

-
-
- - - -
-

[ HOW IT WORKS ]

-
-
01
Install Mac App
Download & run ClickerRemoteReceiver
-
02
Install iOS App
Get ClickerRemote from App Store
-
03
Connect & Present
Same network = Auto connect
-
-
- -
- + From 16cdb33a5367a80a23e6b7f228507c22aaa7fc33 Mon Sep 17 00:00:00 2001 From: donny-son Date: Sun, 1 Mar 2026 12:29:04 +0900 Subject: [PATCH 7/7] delete redundant files --- PRIVACY_POLICY.md | 100 -------------------------------------------- TERMS_OF_SERVICE.md | 90 --------------------------------------- 2 files changed, 190 deletions(-) delete mode 100644 PRIVACY_POLICY.md delete mode 100644 TERMS_OF_SERVICE.md diff --git a/PRIVACY_POLICY.md b/PRIVACY_POLICY.md deleted file mode 100644 index 4e1d4d6..0000000 --- a/PRIVACY_POLICY.md +++ /dev/null @@ -1,100 +0,0 @@ -# Privacy Policy - -**Last updated: January 22, 2026** - -DOU Inc. ("we", "our", or "us") operates the Clicker application (the "App"). This Privacy Policy describes how we collect, use, and protect your information when you use our App. - -## Summary - -**We don't collect your data.** Clicker is designed to work entirely on your devices without requiring any personal information or data transmission to external servers. - -## Information We Do NOT Collect - -Clicker does not collect, store, or transmit: - -- Personal information (name, email, phone number) -- Location data -- Usage analytics or telemetry -- Presentation content or slide data -- Device identifiers for tracking purposes -- Any data to third-party services - -## How Clicker Works - -### Local Communication Only - -Clicker uses Apple's MultipeerConnectivity framework to establish a direct peer-to-peer connection between your iPhone and Mac. This communication: - -- Occurs entirely over your local network (WiFi) or Bluetooth -- Does not pass through any external servers -- Is not logged or recorded by us -- Contains only simple navigation commands (next slide, previous slide) - -### Subscription Management - -If you subscribe to Clicker on iOS: - -- Subscription purchases are processed entirely by Apple through the App Store -- We do not have access to your payment information -- Apple handles all billing, receipts, and subscription management -- We only receive confirmation that a valid subscription exists (no personal details) - -### Local Storage - -Clicker stores minimal data locally on your device: - -- **Trial start date** — Stored in your device's Keychain to track the 7-day trial period -- **Timer preferences** — Your preferred timer settings (duration, haptic interval) - -This data never leaves your device and is not accessible to us. - -## Permissions - -Clicker requests the following permissions: - -| Permission | Platform | Purpose | -|------------|----------|---------| -| **Local Network** | iOS & macOS | To discover and connect to devices on your network | -| **Accessibility** | macOS only | To send keyboard events to your presentation software | - -These permissions are used solely for the App's core functionality and not for data collection. - -## Children's Privacy - -Clicker does not collect any personal information from anyone, including children under 13. - -## Third-Party Services - -Clicker does not integrate with any third-party analytics, advertising, or tracking services. - -The only third-party service involved is: - -- **Apple App Store** — For processing in-app subscriptions (governed by [Apple's Privacy Policy](https://www.apple.com/legal/privacy/)) - -## Data Security - -Since we don't collect data, there is no data to secure on our end. All communication between your devices is handled by Apple's encrypted MultipeerConnectivity framework. - -## Changes to This Policy - -We may update this Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page and updating the "Last updated" date. - -## Contact Us - -If you have questions about this Privacy Policy, please contact us: - -- **Email**: privacy@dou.inc -- **GitHub**: [github.com/douinc/clicker](https://github.com/douinc/clicker) - -## Your Rights - -Depending on your location, you may have certain rights regarding your personal data. Since Clicker does not collect personal data, these rights are inherently satisfied: - -- **Right to Access** — There is no data to access -- **Right to Deletion** — There is no data to delete -- **Right to Portability** — There is no data to export -- **Right to Opt-Out** — There is no data collection to opt out of - ---- - -This Privacy Policy is effective as of January 22, 2026. diff --git a/TERMS_OF_SERVICE.md b/TERMS_OF_SERVICE.md deleted file mode 100644 index 5b6f03a..0000000 --- a/TERMS_OF_SERVICE.md +++ /dev/null @@ -1,90 +0,0 @@ -# Terms of Service - -**Last updated: January 31, 2026** - -Please read these Terms of Service ("Terms") carefully before using the Clicker application (the "App") operated by DOU Inc. ("we", "our", or "us"). - -## Agreement to Terms - -By downloading, installing, or using Clicker, you agree to be bound by these Terms. If you do not agree to these Terms, do not use the App. - -## Description of Service - -Clicker is a presentation remote control application that allows you to control presentations on your Mac using your iPhone. The App uses local network communication between your devices. - -## License - -We grant you a limited, non-exclusive, non-transferable, revocable license to use the App for personal or professional use, subject to these Terms. - -You may NOT: - -- Modify, reverse engineer, or decompile the App -- Use the App for any unlawful purpose -- Redistribute or resell the App -- Remove any copyright or proprietary notices - -## Subscriptions and Payments - -### Free Trial - -New users receive a 7-day free trial with full access to all features. - -### Subscription Terms - -- Subscriptions are processed through Apple's App Store -- Payment is charged to your Apple ID account -- Subscriptions auto-renew unless cancelled at least 24 hours before the end of the current period -- Manage or cancel subscriptions in your Apple ID Account Settings - -### Refunds - -Refund requests must be made through Apple. We do not process refunds directly. - -## Intellectual Property - -Clicker, including its design, logos, and code, is owned by DOU Inc. and protected by copyright and intellectual property laws. All rights not expressly granted are reserved. - -## Disclaimer of Warranties - -THE APP IS PROVIDED "AS IS" WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED. We do not warrant that: - -- The App will be error-free or uninterrupted -- The App will meet your specific requirements -- The App will be compatible with all presentation software - -## Limitation of Liability - -TO THE MAXIMUM EXTENT PERMITTED BY LAW, DOU INC. SHALL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES ARISING FROM YOUR USE OF THE APP. - -Our total liability shall not exceed the amount you paid for the App in the 12 months preceding the claim. - -## Indemnification - -You agree to indemnify and hold harmless DOU Inc. from any claims, damages, or expenses arising from your use of the App or violation of these Terms. - -## Termination - -We reserve the right to terminate or suspend your access to the App at any time, without notice, for conduct that we believe violates these Terms or is harmful to other users. - -## Changes to Terms - -We may modify these Terms at any time. Continued use of the App after changes constitutes acceptance of the modified Terms. We will update the "Last updated" date when changes are made. - -## Governing Law - -These Terms are governed by the laws of the Republic of Korea, without regard to conflict of law provisions. - -## Severability - -If any provision of these Terms is found unenforceable, the remaining provisions will continue in effect. - -## Contact Us - -If you have questions about these Terms, please contact us: - -- **Email**: team@dou.so -- **GitHub**: [github.com/douinc/clicker](https://github.com/douinc/clicker) - ---- - -These Terms of Service are effective as of January 31, 2026.