diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..b81221d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,50 @@ +name: "πŸ› Bug Report" +description: Report a bug or unexpected behavior +labels: [ "bug" ] +body: +- type: textarea + id: description + attributes: + label: Description + description: What happened? What did you expect to happen? + validations: + required: true +- type: textarea + id: steps + attributes: + label: Steps to Reproduce + description: How can we reproduce the issue? + placeholder: | + 1. Open YouTube in Safari + 2. Play a video + 3. Switch to another tab + 4. ... + validations: + required: true +- type: input + id: macos-version + attributes: + label: macOS Version + placeholder: "e.g. 15.4" + validations: + required: true +- type: input + id: safari-version + attributes: + label: Safari Version + placeholder: "e.g. 18.3" + validations: + required: true +- type: input + id: app-version + attributes: + label: AutoPiP Version + description: Shown in the app menu under "About AutoPiP" + placeholder: "e.g. 2.0.0" + validations: + required: true +- type: textarea + id: additional + attributes: + label: Additional Context + description: Screenshots, screen recordings, or anything else that might help. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..3ba13e0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..fc7ef62 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,23 @@ +name: "✨ Feature Request" +description: Suggest an idea or improvement +labels: [ "enhancement" ] +body: +- type: textarea + id: description + attributes: + label: Description + description: What would you like to see added or changed? + validations: + required: true +- type: textarea + id: use-case + attributes: + label: Use Case + description: Why is this feature useful? What problem does it solve? + validations: + required: true +- type: textarea + id: alternatives + attributes: + label: Alternatives Considered + description: Have you considered any workarounds or alternative approaches? diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..657d74c --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,23 @@ +## Description + + + +## Type of Change + +- [ ] πŸ› Bug fix +- [ ] ✨ New feature +- [ ] πŸ”§ Refactoring +- [ ] πŸ“ Documentation +- [ ] πŸ—οΈ CI/CD +- [ ] πŸ”’ Security + +## Checklist + +- [ ] I have tested my changes locally +- [ ] I have updated `semver.txt` (if this change affects the released version) +- [ ] My changes do not introduce new warnings or errors +- [ ] I have updated documentation where necessary + +## Related Issues + + \ No newline at end of file diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml new file mode 100644 index 0000000..88546b9 --- /dev/null +++ b/.github/workflows/build-release.yml @@ -0,0 +1,474 @@ +name: Build and Release + +on: + push: + branches: [main, 'feature/*'] + paths-ignore: + # Skip the workflow entirely when only docs/repo-meta/appcast files changed. + # Source, semver.txt and this workflow itself still trigger a build. + - '**.md' + - 'LICENSE' + - 'PRIVACY.md' + - 'SECURITY.md' + - 'renovate.json' + - '.gitignore' + - '.github/**' + - 'appcast.xml' + workflow_dispatch: + +jobs: + # ── Gate: on main only build when semver.txt changed ───────── + check: + runs-on: ubuntu-latest + outputs: + should-build: ${{ steps.check.outputs.should-build }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 2 + + - name: Decide whether to build + id: check + env: + ACTOR: ${{ github.actor }} + REF_NAME: ${{ github.ref_name }} + BEFORE: ${{ github.event.before }} + HEAD_SHA: ${{ github.sha }} + run: | + # Never build when triggered by the bot itself (e.g. appcast updates). + # Use github.actor β€” github.event.head_commit.author.name is user-spoofable. + # The appcast push is also excluded via paths-ignore at the trigger level. + case "$ACTOR" in + "github-actions[bot]"|"autopip-ci-bot"|*[Bb]ot) + echo "should-build=false" >> "$GITHUB_OUTPUT" + echo "::notice::Skipping β€” actor is $ACTOR (bot)" + exit 0 + ;; + esac + + if [ "$REF_NAME" != "main" ]; then + echo "should-build=true" >> "$GITHUB_OUTPUT" + echo "Feature branch β€” always build" + exit 0 + fi + + if [ "$BEFORE" = "0000000000000000000000000000000000000000" ]; then + echo "should-build=true" >> "$GITHUB_OUTPUT" + echo "New branch push β€” build" + exit 0 + fi + + if git diff --name-only "$BEFORE" "$HEAD_SHA" | grep -q '^semver\.txt$'; then + echo "should-build=true" >> "$GITHUB_OUTPUT" + echo "semver.txt changed on main β€” build" + else + echo "should-build=false" >> "$GITHUB_OUTPUT" + echo "::notice::Skipping β€” semver.txt not changed on main" + fi + + # ── Main build job ─────────────────────────────────────────── + build-and-release: + needs: check + if: needs.check.outputs.should-build == 'true' + runs-on: macos-15 + permissions: + contents: write + concurrency: + group: build-${{ github.ref }} + cancel-in-progress: true + + steps: + # ── Setup ────────────────────────────────────────────────── + - name: Generate GitHub App token + id: app-token + if: vars.CLIENT_ID != '' + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + token: ${{ steps.app-token.outputs.token || github.token }} + + # ── Version & Changelog ──────────────────────────────────── + - name: Read version and changelog from semver.txt + id: version + env: + REF_NAME: ${{ github.ref_name }} + run: | + VERSION=$(head -1 semver.txt | tr -d '[:space:]') + if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::Invalid semver in semver.txt line 1: '$VERSION'" + exit 1 + fi + + # Extract changelog for current version (between --- and next ## or EOF) + awk '/^---$/{found=1; next} /^## /{exit} found{print}' semver.txt \ + | sed '/^[[:space:]]*$/d' > "$RUNNER_TEMP/changelog.txt" + + if [ "$REF_NAME" = "main" ]; then + TAG="v${VERSION}" + PRERELEASE=false + CHANNEL=stable + else + # Auto-increment beta number from existing tags + BETA_NUM=1 + LATEST=$(git tag -l "v${VERSION}-beta*" \ + | sed "s/v${VERSION}-beta//" \ + | grep -E '^[0-9]+$' \ + | sort -n | tail -1) + if [ -n "$LATEST" ]; then + BETA_NUM=$((LATEST + 1)) + fi + TAG="v${VERSION}-beta${BETA_NUM}" + PRERELEASE=true + CHANNEL=beta + fi + + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "prerelease=$PRERELEASE" >> "$GITHUB_OUTPUT" + echo "channel=$CHANNEL" >> "$GITHUB_OUTPUT" + + echo "### $CHANNEL release β€” $TAG" >> "$GITHUB_STEP_SUMMARY" + if [ -s "$RUNNER_TEMP/changelog.txt" ]; then + echo '```' >> "$GITHUB_STEP_SUMMARY" + cat "$RUNNER_TEMP/changelog.txt" >> "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" + fi + + - name: Check for existing tag + run: | + TAG="${{ steps.version.outputs.tag }}" + if git rev-parse "refs/tags/${TAG}" >/dev/null 2>&1; then + echo "::warning::Tag ${TAG} already exists β€” assuming partial release, will reconcile (re-upload asset, refresh appcast)." + echo "tag_exists=true" >> "$GITHUB_OUTPUT" + else + echo "tag_exists=false" >> "$GITHUB_OUTPUT" + fi + + # ── Signing ──────────────────────────────────────────────── + - name: Import signing certificate + env: + BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} + P12_PASSWORD: ${{ secrets.P12_PASSWORD }} + run: | + if [ -z "$BUILD_CERTIFICATE_BASE64" ]; then + echo "::error::BUILD_CERTIFICATE_BASE64 secret is not set β€” code signing is required." + exit 1 + fi + + CERTIFICATE_PATH="$RUNNER_TEMP/build_certificate.p12" + KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db" + KEYCHAIN_PASSWORD="$(openssl rand -base64 32)" + + echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o "$CERTIFICATE_PATH" + + security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" + security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + + security import "$CERTIFICATE_PATH" -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" + security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security list-keychain -d user -s "$KEYCHAIN_PATH" + + echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV" + + # ── Build ────────────────────────────────────────────────── + - name: Set version in Xcode project + env: + VERSION: ${{ steps.version.outputs.version }} + BUILD: ${{ github.run_number }} + run: | + BUILD=$(( BUILD + 10 )) + + sed -i '' "s/MARKETING_VERSION = .*;/MARKETING_VERSION = ${VERSION};/" \ + AutoPiP.xcodeproj/project.pbxproj + sed -i '' "s/CURRENT_PROJECT_VERSION = .*;/CURRENT_PROJECT_VERSION = ${BUILD};/" \ + AutoPiP.xcodeproj/project.pbxproj + + sed -i '' "s/\"version\": \"[^\"]*\"/\"version\": \"${VERSION}\"/" \ + "AutoPiP Extension/Resources/manifest.json" + + echo "MARKETING_VERSION=$VERSION CURRENT_PROJECT_VERSION=$BUILD" + + - name: Resolve Swift packages + run: | + xcodebuild -resolvePackageDependencies \ + -project AutoPiP.xcodeproj \ + -scheme AutoPiP + + - name: Archive + run: | + ARCHIVE_PATH="$RUNNER_TEMP/AutoPiP.xcarchive" + + xcodebuild archive \ + -project AutoPiP.xcodeproj \ + -scheme AutoPiP \ + -configuration Release \ + -archivePath "$ARCHIVE_PATH" + + - name: Create DMG + run: | + APP_PATH="$RUNNER_TEMP/AutoPiP.xcarchive/Products/Applications/AutoPiP.app" + + if [ ! -d "$APP_PATH" ]; then + echo "::error::AutoPiP.app not found in archive" + exit 1 + fi + + brew install create-dmg + + create-dmg \ + --volname "AutoPiP" \ + --window-pos 200 120 \ + --window-size 600 400 \ + --icon-size 100 \ + --icon "AutoPiP.app" 175 190 \ + --app-drop-link 425 190 \ + "$RUNNER_TEMP/AutoPiP.dmg" \ + "$APP_PATH" + + echo "AutoPiP.dmg β€” $(du -h "$RUNNER_TEMP/AutoPiP.dmg" | cut -f1)" + + # ── Sparkle ──────────────────────────────────────────────── + - name: Sign DMG and prepare Sparkle appcast + env: + SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }} + VERSION: ${{ steps.version.outputs.version }} + BUILD: ${{ github.run_number }} + run: | + BUILD=$(( BUILD + 10 )) + TAG="${{ steps.version.outputs.tag }}" + CHANNEL="${{ steps.version.outputs.channel }}" + DMG_PATH="$RUNNER_TEMP/AutoPiP.dmg" + + FILE_LENGTH=$(stat -f%z "$DMG_PATH") + echo "DMG size: $FILE_LENGTH bytes" + + # ── Sparkle EdDSA signature (required) ── + if [ -z "$SPARKLE_PRIVATE_KEY" ]; then + echo "::error::SPARKLE_PRIVATE_KEY secret is not set β€” Sparkle signing is required." + exit 1 + fi + + echo "Downloading Sparkle tools…" + curl -fSL \ + "https://github.com/sparkle-project/Sparkle/releases/download/2.9.1/Sparkle-2.9.1.tar.xz" \ + -o "$RUNNER_TEMP/sparkle.tar.xz" + mkdir -p "$RUNNER_TEMP/sparkle" + tar -xJf "$RUNNER_TEMP/sparkle.tar.xz" -C "$RUNNER_TEMP/sparkle" + + SIGN_TOOL="$RUNNER_TEMP/sparkle/bin/sign_update" + if [ ! -x "$SIGN_TOOL" ]; then + echo "::error::sign_update not found or not executable at $SIGN_TOOL" + exit 1 + fi + + echo "Signing DMG with Sparkle EdDSA…" + KEY_FILE="$RUNNER_TEMP/sparkle_ed_key" + echo "$SPARKLE_PRIVATE_KEY" > "$KEY_FILE" + SIGN_OUTPUT=$("$SIGN_TOOL" "$DMG_PATH" --ed-key-file "$KEY_FILE" 2>&1) || { + echo "::error::sign_update failed:" + echo "$SIGN_OUTPUT" + rm -f "$KEY_FILE" + exit 1 + } + rm -f "$KEY_FILE" + SIGNATURE=$(echo "$SIGN_OUTPUT" | grep -oE 'sparkle:edSignature="[^"]+"' | cut -d'"' -f2) + if [ -z "$SIGNATURE" ]; then + echo "::error::Sparkle signing failed β€” no edSignature found in output:" + echo "$SIGN_OUTPUT" + exit 1 + fi + SIG_ATTR=$'\n'" sparkle:edSignature=\"${SIGNATURE}\"" + echo "βœ… Sparkle EdDSA signature generated" + + # ── Build appcast XML ── + { + echo ' ' + + if [ "$CHANNEL" = "beta" ]; then + echo " Version ${VERSION} (Beta)" + echo ' beta' + else + echo " Version ${VERSION}" + fi + + # Description from changelog + if [ -s "$RUNNER_TEMP/changelog.txt" ]; then + echo ' ' + echo ' ' + echo ' ⚠️ Beta Release β€” This version may be unstable. Only install if you want to test new features early.' + echo '

' + fi + + echo '
    ' + while IFS= read -r line; do + case "$line" in + "- "*) echo "
  • ${line#- }
  • " ;; + "### "*) echo "

${line#\#\#\# }

    " ;; + esac + done < "$RUNNER_TEMP/changelog.txt" + echo '
' + echo ' ]]>' + echo '
' + fi + + DOWNLOAD_URL="https://github.com/vordenken/AutoPiP/releases/download/${TAG}/AutoPiP.dmg" + PUB_DATE=$(date -u '+%a, %d %b %Y %H:%M:%S %z') + # For betas, use the full tag (e.g. "2.1.0-beta12") as display version + if [ "$CHANNEL" = "beta" ]; then + DISPLAY_VERSION="${TAG#v}" + else + DISPLAY_VERSION="${VERSION}" + fi + + echo " ${PUB_DATE}" + echo ' ' + echo '
' + } > "$RUNNER_TEMP/appcast_item.xml" + + # Remove existing entries for the same shortVersionString (dedup) + awk -v ver="$VERSION" ' + // { buf = $0; inside = 1; next } + inside { + buf = buf ORS $0 + if (/<\/item>/) { + inside = 0 + if (buf !~ "sparkle:shortVersionString=\"" ver "\"") print buf + buf = "" + } + next + } + { print } + ' appcast.xml > "$RUNNER_TEMP/appcast_deduped.xml" + mv "$RUNNER_TEMP/appcast_deduped.xml" appcast.xml + + # Insert after AutoPiP Updates + sed -i '' "/AutoPiP Updates<\/title>/r $RUNNER_TEMP/appcast_item.xml" appcast.xml + echo "appcast.xml updated β€” $CHANNEL ${TAG}" + + - name: Upload build artifact + uses: actions/upload-artifact@v7 + with: + name: AutoPiP + path: ${{ runner.temp }}/AutoPiP.dmg + + # ── Release ──────────────────────────────────────────────── + - name: Prepare release notes + run: | + NOTES="$RUNNER_TEMP/release_notes.md" + + # Prepend beta notice for pre-releases + if [ "${{ steps.version.outputs.prerelease }}" = "true" ]; then + { + echo "> [!WARNING]" + echo "> **This is a beta release.** It may be unstable or incomplete. Only install if you want to test new features early." + echo "" + } > "$NOTES" + else + : > "$NOTES" + fi + + # Append changelog if available + if [ -s "$RUNNER_TEMP/changelog.txt" ]; then + { + echo "## Changelog" + echo "" + cat "$RUNNER_TEMP/changelog.txt" + echo "" + echo "---" + echo "" + } >> "$NOTES" + fi + + cat >> "$NOTES" <<'INSTALL_EOF' + ## Important Note + + This extension is currently not notarized by Apple. You'll need to bypass macOS security settings to initially install it. Updating works through Sparkle and doesn't need these steps again! + + ### Installation Steps + + 1. Download `AutoPiP.dmg` file from the releases page and open it + 2. When trying to open `AutoPiP.dmg`, macOS will show a security warning + 3. To bypass this: + - Open System Settings + - Go to Privacy & Security + - Scroll down and click "Open Anyway" + 4. Open `AutoPiP.dmg` again + 5. Copy the `.app` to your Applications folder + 6. When trying to open the app, macOS will show a security warning + 7. To bypass this: + - Open System Settings + - Go to Privacy & Security + - Scroll down and click "Open Anyway" + 8. Open `AutoPiP.app` again + 9. Follow the instructions + 10. Allow the requested permissions when prompted + INSTALL_EOF + + - name: Create tag and GitHub Release + uses: softprops/action-gh-release@v3 + with: + tag_name: ${{ steps.version.outputs.tag }} + name: ${{ steps.version.outputs.tag }} + files: ${{ runner.temp }}/AutoPiP.dmg + prerelease: ${{ steps.version.outputs.prerelease == 'true' }} + body_path: ${{ runner.temp }}/release_notes.md + token: ${{ steps.app-token.outputs.token || github.token }} + + # ── Push appcast ─────────────────────────────────────────── + - name: Push appcast to working branch and main + env: + GH_TOKEN: ${{ steps.app-token.outputs.token || github.token }} + WORKING_BRANCH: ${{ github.ref_name }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + TAG="${{ steps.version.outputs.tag }}" + + # Save updated appcast + cp appcast.xml "$RUNNER_TEMP/appcast.xml" + git checkout -- . + + push_appcast() { + local branch="$1" + git checkout "$branch" + cp "$RUNNER_TEMP/appcast.xml" appcast.xml + git add appcast.xml + if git diff --cached --quiet; then + echo "appcast.xml unchanged on $branch β€” nothing to push" + return 0 + fi + git commit -m "chore: update appcast for ${TAG}" + git push origin "$branch" + echo "βœ… appcast.xml pushed to $branch" + } + + push_appcast "$WORKING_BRANCH" + + if [ "$WORKING_BRANCH" != "main" ]; then + git fetch origin main + push_appcast "main" + fi + + # ── Cleanup ──────────────────────────────────────────────── + - name: Cleanup keychain + if: always() + run: | + if [ -n "$KEYCHAIN_PATH" ] && [ -f "$KEYCHAIN_PATH" ]; then + security delete-keychain "$KEYCHAIN_PATH" || true + fi diff --git a/AutoPiP Extension/Resources/content.js b/AutoPiP Extension/Resources/content.js index aa520e9..1ea5c6d 100644 --- a/AutoPiP Extension/Resources/content.js +++ b/AutoPiP Extension/Resources/content.js @@ -574,12 +574,29 @@ function togglePiP() { disablePiP(); } else { debugLog('Keyboard shortcut: enabling PiP (manual override)'); - try { - if (!setWebkitPresentationMode(video, 'picture-in-picture')) { - debugLog('PiP not supported on this video element'); + // Prefer the W3C requestPictureInPicture API here: unlike webkitSetPresentationMode, + // it uses the video's natural (intrinsic) dimensions rather than its CSS-rendered size. + // This prevents the "video small in corner" bug when e.g. YouTube's mini-player is + // active and the video element is currently rendered at a small size. + if (typeof video.requestPictureInPicture === 'function') { + video.requestPictureInPicture().catch(error => { + debugLog('requestPictureInPicture failed, falling back to webkit API:', error.message); + try { + if (!setWebkitPresentationMode(video, 'picture-in-picture')) { + debugLog('PiP not supported on this video element'); + } + } catch (e) { + console.error('[AutoPiP] Keyboard PiP toggle exception:', e.message || e); + } + }); + } else { + try { + if (!setWebkitPresentationMode(video, 'picture-in-picture')) { + debugLog('PiP not supported on this video element'); + } + } catch (error) { + console.error('[AutoPiP] Keyboard PiP toggle exception:', error.message || error); } - } catch (error) { - console.error('[AutoPiP] Keyboard PiP toggle exception:', error.message || error); } } } diff --git a/AutoPiP Extension/Resources/manifest.json b/AutoPiP Extension/Resources/manifest.json index 3062195..867dd64 100644 --- a/AutoPiP Extension/Resources/manifest.json +++ b/AutoPiP Extension/Resources/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "AutoPiP", - "version": "2.0.0", + "version": "2.1.0", "default_locale": "en", "description": "Automatically enables Picture-in-Picture mode when switching tabs, windows or scrolling down YouTube videos", "icons": { diff --git a/AutoPiP.xcodeproj/project.pbxproj b/AutoPiP.xcodeproj/project.pbxproj index 177ff77..d9586b2 100644 --- a/AutoPiP.xcodeproj/project.pbxproj +++ b/AutoPiP.xcodeproj/project.pbxproj @@ -428,7 +428,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.5; - MARKETING_VERSION = 2.0.0; + MARKETING_VERSION = 2.1.0; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -462,7 +462,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.5; - MARKETING_VERSION = 2.0.0; + MARKETING_VERSION = 2.1.0; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -634,7 +634,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.5; - MARKETING_VERSION = 2.0.0; + MARKETING_VERSION = 2.1.0; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -681,7 +681,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.5; - MARKETING_VERSION = 2.0.0; + MARKETING_VERSION = 2.1.0; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -706,7 +706,7 @@ DEVELOPMENT_TEAM = NRKZZ9TFF7; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 13.5; - MARKETING_VERSION = 2.0.0; + MARKETING_VERSION = 2.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.vd.AutoPiPTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; @@ -724,7 +724,7 @@ DEVELOPMENT_TEAM = NRKZZ9TFF7; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 13.5; - MARKETING_VERSION = 2.0.0; + MARKETING_VERSION = 2.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.vd.AutoPiPTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; @@ -741,7 +741,7 @@ DEVELOPMENT_TEAM = NRKZZ9TFF7; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 13.5; - MARKETING_VERSION = 2.0.0; + MARKETING_VERSION = 2.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.vd.AutoPiPUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; @@ -758,7 +758,7 @@ DEVELOPMENT_TEAM = NRKZZ9TFF7; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 13.5; - MARKETING_VERSION = 2.0.0; + MARKETING_VERSION = 2.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.vd.AutoPiPUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; diff --git a/AutoPiP.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/AutoPiP.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 6e450d9..190626a 100644 --- a/AutoPiP.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/AutoPiP.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/sparkle-project/Sparkle", "state" : { - "revision" : "21d8df80440b1ca3b65fa82e40782f1e5a9e6ba2", - "version" : "2.9.0" + "revision" : "d46d456107feacc80711b21847b82b07bd9fb46e", + "version" : "2.9.3" } } ], diff --git a/AutoPiP/AppDelegate.swift b/AutoPiP/AppDelegate.swift index 3b8e600..261b139 100644 --- a/AutoPiP/AppDelegate.swift +++ b/AutoPiP/AppDelegate.swift @@ -10,7 +10,7 @@ import Cocoa @main class AppDelegate: NSObject, NSApplicationDelegate { - private let updateController = UpdateController() + let updateController = UpdateController() func applicationDidFinishLaunching(_ notification: Notification) { setupMenu() diff --git a/AutoPiP/Info.plist b/AutoPiP/Info.plist index ce061c6..7b099db 100644 --- a/AutoPiP/Info.plist +++ b/AutoPiP/Info.plist @@ -2,6 +2,8 @@ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> + <key>SUEnableAutomaticChecks</key> + <true/> <key>SUEnableInstallerLauncherService</key> <true/> <key>SUFeedURL</key> diff --git a/AutoPiP/Resources/Base.lproj/Main.html b/AutoPiP/Resources/Base.lproj/Main.html index 3410697..dd5a5cf 100644 --- a/AutoPiP/Resources/Base.lproj/Main.html +++ b/AutoPiP/Resources/Base.lproj/Main.html @@ -3,17 +3,119 @@ <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta http-equiv="Content-Security-Policy" content="default-src 'self'"> - <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"> - <link rel="stylesheet" href="../Style.css"> <script src="../Script.js" defer></script> </head> <body> - <img src="../Icon.png" width="128" height="128" alt="AutoPiP Icon"> - <p class="state-unknown">You can turn on AutoPiP’s extension in Safari Extensions preferences.</p> - <p class="state-on">AutoPiP’s extension is currently on. You can turn it off in Safari Extensions preferences.</p> - <p class="state-off">AutoPiP’s extension is currently off. You can turn it on in Safari Extensions preferences.</p> - <button class="open-preferences">Quit and Open Safari Extensions Preferences…</button> + + <!-- ===== ONBOARDING PAGE 1: Welcome ===== --> + <div class="page hidden" id="page-welcome"> + <div class="page-content centered"> + <img src="../Icon.png" width="96" height="96" alt="AutoPiP Icon"> + <h1>Welcome to AutoPiP</h1> + <p class="subtitle">Automatically enables Picture-in-Picture when you switch tabs or windows in Safari.</p> + <div class="support-links"> + <a href="https://github.com/vordenken/AutoPiP" id="link-github">GitHub</a> + <span class="dot">·</span> + <a href="https://ko-fi.com/vordenken" id="link-kofi">Ko-fi</a> + <span class="dot">·</span> + <a href="https://www.buymeacoffee.com/vordenken" id="link-bmc">Buy Me a Coffee</a> + </div> + </div> + <div class="page-footer"> + <span class="version-label" id="version-label"></span> + <button id="welcome-next" class="btn primary">Continue</button> + </div> + </div> + + <!-- ===== ONBOARDING PAGE 2: Features ===== --> + <div class="page hidden" id="page-features"> + <div class="page-content"> + <h2>Features</h2> + <p class="hint">Configure these anytime in the AutoPiP Safari toolbar popup.</p> + <div class="feature-list"> + <div class="feature-item"> + <strong>Automatic PiP</strong> + <span>Switch tabs or windows – your video continues in PiP.</span> + </div> + <div class="feature-item"> + <strong>Scroll PiP</strong> + <span>Scroll past a YouTube video to trigger PiP automatically.</span> + </div> + <div class="feature-item"> + <strong>Keyboard Shortcut</strong> + <span>Press ⌥P to toggle PiP at any time (customizable).</span> + </div> + <div class="feature-item"> + <strong>Site Control</strong> + <span>Blacklist mode (default) works on all sites. Switch to Whitelist to allow only specific ones.</span> + </div> + </div> + </div> + <div class="page-footer"> + <button id="features-back" class="btn">Back</button> + <button id="features-next" class="btn primary">Continue</button> + </div> + </div> + + <!-- ===== ONBOARDING PAGE 3: Updates + Done ===== --> + <div class="page hidden" id="page-updates"> + <div class="page-content"> + <h2>Updates</h2> + <div class="settings-list"> + <div class="setting-row"> + <span>Automatically check for updates</span> + <input type="checkbox" class="toggle" id="onb-auto-check" checked> + </div> + <div class="setting-row sub"> + <span>Automatically download updates</span> + <input type="checkbox" class="toggle" id="onb-auto-download"> + </div> + <div class="setting-row"> + <span>Include beta updates</span> + <input type="checkbox" class="toggle" id="onb-beta"> + </div> + </div> + <button id="onb-check-updates" class="btn">Check for Updates Now</button> + <div class="separator"></div> + <p class="hint permission-hint">When Safari opens, enable the AutoPiP extension and allow it access to <strong>all websites</strong>.</p> + </div> + <div class="page-footer"> + <button id="updates-back" class="btn">Back</button> + <button id="updates-done" class="btn primary">Done – Open Safari</button> + </div> + </div> + + <!-- ===== MAIN VIEW (after onboarding) ===== --> + <div class="page hidden" id="page-main"> + <div class="page-content"> + <img src="../Icon.png" width="64" height="64" alt="AutoPiP Icon"> + <p class="state-info state-unknown">You can turn on AutoPiP’s extension in Safari Extensions preferences.</p> + <p class="state-info state-on">AutoPiP’s extension is currently on. You can turn it off in Safari Extensions preferences.</p> + <p class="state-info state-off">AutoPiP’s extension is currently off. You can turn it on in Safari Extensions preferences.</p> + <div class="separator"></div> + <div class="settings-list"> + <div class="setting-row"> + <span>Automatically check for updates</span> + <input type="checkbox" class="toggle" id="auto-check-toggle"> + </div> + <div class="setting-row sub"> + <span>Automatically download updates</span> + <input type="checkbox" class="toggle" id="auto-download-toggle"> + </div> + <div class="setting-row"> + <span>Include beta updates</span> + <input type="checkbox" class="toggle" id="beta-toggle"> + </div> + </div> + <button id="check-updates-btn" class="btn">Check for Updates Now</button> + </div> + <div class="page-footer"> + <span class="version-label" id="main-version-label"></span> + <button class="open-preferences btn primary">Open Safari Preferences…</button> + </div> + </div> + </body> </html> diff --git a/AutoPiP/Resources/Script.js b/AutoPiP/Resources/Script.js index eaee639..83a23ea 100644 --- a/AutoPiP/Resources/Script.js +++ b/AutoPiP/Resources/Script.js @@ -1,22 +1,135 @@ +/* ===== Onboarding Navigation ===== */ + +function showPage(id) { + document.querySelectorAll('.page').forEach(function(p) { p.classList.add('hidden'); }); + document.getElementById(id).classList.remove('hidden'); +} + +function startOnboarding() { + showPage('page-welcome'); +} + +/* ===== Version (called from Swift) ===== */ + +function setVersion(v) { + var text = 'v' + v; + var el = document.getElementById('version-label'); + if (el) el.textContent = text; + var el2 = document.getElementById('main-version-label'); + if (el2) el2.textContent = text; +} + +/* ===== Main view (called from Swift) ===== */ + function show(enabled, useSettingsInsteadOfPreferences) { + showPage('page-main'); + if (useSettingsInsteadOfPreferences) { - document.getElementsByClassName('state-on')[0].innerText = "AutoPiP’s extension is currently on. You can turn it off in the Extensions section of Safari Settings."; - document.getElementsByClassName('state-off')[0].innerText = "AutoPiP’s extension is currently off. You can turn it on in the Extensions section of Safari Settings."; - document.getElementsByClassName('state-unknown')[0].innerText = "You can turn on AutoPiP’s extension in the Extensions section of Safari Settings."; - document.getElementsByClassName('open-preferences')[0].innerText = "Quit and Open Safari Settings…"; + document.getElementsByClassName('state-on')[0].innerText = "AutoPiP\u2019s extension is currently on. You can turn it off in the Extensions section of Safari Settings."; + document.getElementsByClassName('state-off')[0].innerText = "AutoPiP\u2019s extension is currently off. You can turn it on in the Extensions section of Safari Settings."; + document.getElementsByClassName('state-unknown')[0].innerText = "You can turn on AutoPiP\u2019s extension in the Extensions section of Safari Settings."; + document.getElementsByClassName('open-preferences')[0].innerText = "Open Safari Settings\u2026"; } if (typeof enabled === "boolean") { - document.body.classList.toggle(`state-on`, enabled); - document.body.classList.toggle(`state-off`, !enabled); + document.body.classList.toggle('state-on', enabled); + document.body.classList.toggle('state-off', !enabled); } else { - document.body.classList.remove(`state-on`); - document.body.classList.remove(`state-off`); + document.body.classList.remove('state-on'); + document.body.classList.remove('state-off'); } } -function openPreferences() { - webkit.messageHandlers.controller.postMessage("open-preferences"); +function setUpdateSettings(settings) { + document.getElementById('auto-check-toggle').checked = settings.autoCheck; + document.getElementById('auto-download-toggle').checked = settings.autoDownload; + document.getElementById('auto-download-toggle').disabled = !settings.autoCheck; + document.getElementById('beta-toggle').checked = settings.beta; } -document.querySelector("button.open-preferences").addEventListener("click", openPreferences); +/* ===== Event Listeners ===== */ + +document.addEventListener('DOMContentLoaded', function() { + + /* --- Onboarding Page 1 --- */ + document.getElementById('welcome-next').addEventListener('click', function() { + showPage('page-features'); + }); + + /* --- Onboarding Page 2 (Features) --- */ + document.getElementById('features-back').addEventListener('click', function() { + showPage('page-welcome'); + }); + document.getElementById('features-next').addEventListener('click', function() { + showPage('page-updates'); + }); + + /* --- Onboarding Page 3 (Updates) --- */ + document.getElementById('updates-back').addEventListener('click', function() { + showPage('page-features'); + }); + + document.getElementById('onb-auto-check').addEventListener('change', function() { + var dl = document.getElementById('onb-auto-download'); + dl.disabled = !this.checked; + if (!this.checked) dl.checked = false; + webkit.messageHandlers.controller.postMessage("set-auto-check:" + this.checked); + if (!this.checked) webkit.messageHandlers.controller.postMessage("set-auto-download:false"); + }); + + document.getElementById('onb-auto-download').addEventListener('change', function() { + webkit.messageHandlers.controller.postMessage("set-auto-download:" + this.checked); + }); + + document.getElementById('onb-beta').addEventListener('change', function() { + webkit.messageHandlers.controller.postMessage("set-beta:" + this.checked); + }); + + document.getElementById('onb-check-updates').addEventListener('click', function() { + webkit.messageHandlers.controller.postMessage("check-for-updates"); + }); + + document.getElementById('updates-done').addEventListener('click', function() { + var settings = { + autoCheck: document.getElementById('onb-auto-check').checked, + autoDownload: document.getElementById('onb-auto-download').checked, + beta: document.getElementById('onb-beta').checked + }; + webkit.messageHandlers.controller.postMessage("onboarding-done:" + JSON.stringify(settings)); + }); + + /* --- Support links (open in default browser) --- */ + document.querySelectorAll('.support-links a').forEach(function(link) { + link.addEventListener('click', function(e) { + e.preventDefault(); + webkit.messageHandlers.controller.postMessage("open-url:" + this.href); + }); + }); + + /* --- Main view controls --- */ + document.querySelector("button.open-preferences").addEventListener("click", function() { + webkit.messageHandlers.controller.postMessage("open-preferences"); + }); + + document.getElementById('check-updates-btn').addEventListener('click', function() { + webkit.messageHandlers.controller.postMessage("check-for-updates"); + }); + + document.getElementById('auto-check-toggle').addEventListener('change', function() { + webkit.messageHandlers.controller.postMessage("set-auto-check:" + this.checked); + var dl = document.getElementById('auto-download-toggle'); + dl.disabled = !this.checked; + if (!this.checked) { + dl.checked = false; + webkit.messageHandlers.controller.postMessage("set-auto-download:false"); + } + }); + + document.getElementById('auto-download-toggle').addEventListener('change', function() { + webkit.messageHandlers.controller.postMessage("set-auto-download:" + this.checked); + }); + + document.getElementById('beta-toggle').addEventListener('change', function() { + webkit.messageHandlers.controller.postMessage("set-beta:" + this.checked); + }); +}); diff --git a/AutoPiP/Resources/Style.css b/AutoPiP/Resources/Style.css index cbde9e6..4a93697 100644 --- a/AutoPiP/Resources/Style.css +++ b/AutoPiP/Resources/Style.css @@ -6,40 +6,266 @@ :root { color-scheme: light dark; + --accent-color: rgb(0, 122, 255); + --success-color: #34c759; + --border-color: rgba(128, 128, 128, 0.25); +} - --spacing: 20px; +@media (prefers-color-scheme: dark) { + :root { + --accent-color: #0a84ff; + --success-color: #32d74b; + --border-color: rgba(128, 128, 128, 0.35); + } } -html { +html, body { height: 100%; + margin: 0; + overflow: hidden; } body { + font: -apple-system-short-body; + text-align: center; +} + +.hidden { display: none !important; } + +/* ===== Pages (shared) ===== */ + +.page { display: flex; + flex-direction: column; + height: 100%; +} + +.page-content { + flex: 1; + display: flex; + flex-direction: column; align-items: center; justify-content: center; - flex-direction: column; + padding: 20px 30px 0; + gap: 8px; +} - gap: var(--spacing); - margin: 0 calc(var(--spacing) * 2); - height: 100%; +.page-content.centered { + text-align: center; +} - font: -apple-system-short-body; +.page-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 20px 14px; +} + +/* ===== Typography ===== */ + +h1 { + font-size: 1.3em; + margin: 0; + font-weight: 600; +} + +h2 { + font-size: 1.1em; + margin: 0 0 2px; + font-weight: 600; +} + +.subtitle { + margin: 0; + font-size: 0.85em; + opacity: 0.7; + line-height: 1.4; +} + +.hint { + margin: 0; + font-size: 0.75em; + opacity: 0.55; +} + +.permission-hint { text-align: center; + line-height: 1.4; +} + +/* ===== Buttons ===== */ + +.btn { + font-size: 0.85em; + padding: 5px 14px; + border-radius: 5px; + border: none; + cursor: default; + background: rgba(128, 128, 128, 0.2); +} + +.btn:active { + opacity: 0.7; +} + +.btn.primary { + background-color: var(--accent-color); + color: white; + font-weight: 500; +} + + + +/* ===== Toggle Switch (matches popup) ===== */ + +.toggle { + position: relative; + width: 38px; + height: 22px; + -webkit-appearance: none; + appearance: none; + background-color: rgba(128, 128, 128, 0.3); + border-radius: 11px; + outline: none; + cursor: default; + transition: background-color 0.25s ease; + margin: 0; + flex-shrink: 0; +} + +.toggle:checked { + background-color: var(--success-color); +} + +.toggle::before { + content: ''; + position: absolute; + width: 18px; + height: 18px; + border-radius: 50%; + background-color: white; + top: 2px; + left: 2px; + transition: transform 0.25s ease; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); +} + +.toggle:checked::before { + transform: translateX(16px); } +.toggle:active::before { + width: 22px; +} + +/* ===== Setting rows ===== */ + +.settings-list { + width: 100%; + display: flex; + flex-direction: column; + gap: 6px; + text-align: left; + font-size: 0.85em; +} + +.setting-row { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; +} + +.setting-row.sub { + padding-left: 18px; +} + +.setting-row span { + flex: 1; +} + +/* ===== Feature list (page 2) ===== */ + +.feature-list { + width: 100%; + display: flex; + flex-direction: column; + gap: 10px; + text-align: left; +} + +.feature-item { + display: flex; + flex-direction: column; + gap: 1px; + font-size: 0.85em; +} + +.feature-item strong { + font-weight: 600; +} + +.feature-item span { + font-size: 0.88em; + opacity: 0.55; + line-height: 1.3; +} + +/* ===== Support links ===== */ + +.support-links { + display: flex; + gap: 6px; + align-items: center; + font-size: 0.75em; + margin-top: 10px; +} + +.support-links a { + color: var(--accent-color); + text-decoration: none; +} + +.support-links a:hover { + text-decoration: underline; +} + +.dot { + opacity: 0.4; +} + +/* ===== Version label ===== */ + +.version-label { + font-size: 0.7em; + opacity: 0.35; +} + +/* ===== Separator ===== */ + +.separator { + width: 100%; + height: 1px; + background: var(--border-color); + margin: 4px 0; +} + +/* ===== State info text (main view) ===== */ + +.state-info { + margin: 0; + font-size: 0.85em; + opacity: 0.7; + line-height: 1.4; +} + +/* State visibility */ body:not(.state-on, .state-off) :is(.state-on, .state-off) { display: none; } - body.state-on :is(.state-off, .state-unknown) { display: none; } - body.state-off :is(.state-on, .state-unknown) { display: none; } - -button { - font-size: 1em; -} diff --git a/AutoPiP/UpdateController.swift b/AutoPiP/UpdateController.swift index f1c0792..f4800e0 100644 --- a/AutoPiP/UpdateController.swift +++ b/AutoPiP/UpdateController.swift @@ -3,14 +3,22 @@ import Sparkle +private class UpdaterDelegate: NSObject, SPUUpdaterDelegate { + func allowedChannels(for updater: SPUUpdater) -> Set<String> { + UserDefaults.standard.bool(forKey: "BetaUpdatesEnabled") ? Set(["beta"]) : Set() + } +} + class UpdateController { + private let delegate = UpdaterDelegate() private let updaterController: SPUStandardUpdaterController + var updater: SPUUpdater { updaterController.updater } + init() { - // Initialisiere den Updater updaterController = SPUStandardUpdaterController( startingUpdater: true, - updaterDelegate: nil, + updaterDelegate: delegate, userDriverDelegate: nil ) } @@ -18,4 +26,28 @@ class UpdateController { func checkForUpdates() { updaterController.checkForUpdates(nil) } + + var automaticallyChecksForUpdates: Bool { + get { updater.automaticallyChecksForUpdates } + set { + updater.automaticallyChecksForUpdates = newValue + if !newValue { updater.automaticallyDownloadsUpdates = false } + } + } + + var automaticallyDownloadsUpdates: Bool { + get { updater.automaticallyDownloadsUpdates } + set { + if newValue { updater.automaticallyChecksForUpdates = true } + updater.automaticallyDownloadsUpdates = newValue + } + } + + var isBetaUpdatesEnabled: Bool { + get { UserDefaults.standard.bool(forKey: "BetaUpdatesEnabled") } + set { + UserDefaults.standard.set(newValue, forKey: "BetaUpdatesEnabled") + updater.resetUpdateCycleAfterShortDelay() + } + } } diff --git a/AutoPiP/ViewController.swift b/AutoPiP/ViewController.swift index ff83c74..a5ddfe2 100644 --- a/AutoPiP/ViewController.swift +++ b/AutoPiP/ViewController.swift @@ -26,30 +26,111 @@ class ViewController: NSViewController, WKNavigationDelegate, WKScriptMessageHan } func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - SFSafariExtensionManager.getStateOfSafariExtension(withIdentifier: extensionBundleIdentifier) { (state, error) in - guard let state = state, error == nil else { - // Insert code to inform the user that something went wrong. - return - } + if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String { + webView.evaluateJavaScript("setVersion('\(version)')") + } + + let onboardingDone = UserDefaults.standard.bool(forKey: "OnboardingCompleted") + + if !onboardingDone { + webView.evaluateJavaScript("startOnboarding()") + return + } + + showMainView() + } + private func showMainView() { + SFSafariExtensionManager.getStateOfSafariExtension(withIdentifier: extensionBundleIdentifier) { (state, error) in DispatchQueue.main.async { + guard let state = state, error == nil else { + self.webView.evaluateJavaScript("show(null, true)") + return + } + if #available(macOS 13, *) { - webView.evaluateJavaScript("show(\(state.isEnabled), true)") + self.webView.evaluateJavaScript("show(\(state.isEnabled), true)") } else { - webView.evaluateJavaScript("show(\(state.isEnabled), false)") + self.webView.evaluateJavaScript("show(\(state.isEnabled), false)") } } } + + if let appDelegate = NSApp.delegate as? AppDelegate { + let uc = appDelegate.updateController + let json = """ + {autoCheck:\(uc.automaticallyChecksForUpdates),autoDownload:\(uc.automaticallyDownloadsUpdates),beta:\(uc.isBetaUpdatesEnabled)} + """ + webView.evaluateJavaScript("setUpdateSettings(\(json))") + } } func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { - if (message.body as! String != "open-preferences") { - return; + guard let body = message.body as? String else { return } + + if body == "open-preferences" { + openSafariAndQuit() + return + } + + if body.hasPrefix("open-url:") { + let urlString = String(body.dropFirst("open-url:".count)) + if let url = URL(string: urlString) { + NSWorkspace.shared.open(url) + } + return + } + + if body.hasPrefix("onboarding-done:") { + let jsonString = String(body.dropFirst("onboarding-done:".count)) + handleOnboardingDone(jsonString) + return + } + + guard let uc = (NSApp.delegate as? AppDelegate)?.updateController else { return } + + switch body { + case "check-for-updates": + uc.checkForUpdates() + case let s where s.hasPrefix("set-auto-check:"): + uc.automaticallyChecksForUpdates = s.hasSuffix("true") + case let s where s.hasPrefix("set-auto-download:"): + uc.automaticallyDownloadsUpdates = s.hasSuffix("true") + case let s where s.hasPrefix("set-beta:"): + uc.isBetaUpdatesEnabled = s.hasSuffix("true") + default: + break + } + } + + private func handleOnboardingDone(_ jsonString: String) { + guard let data = jsonString.data(using: .utf8), + let settings = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let uc = (NSApp.delegate as? AppDelegate)?.updateController else { return } + + if let autoCheck = settings["autoCheck"] as? Bool { + uc.automaticallyChecksForUpdates = autoCheck + } + if let autoDownload = settings["autoDownload"] as? Bool { + uc.automaticallyDownloadsUpdates = autoDownload + } + if let beta = settings["beta"] as? Bool { + uc.isBetaUpdatesEnabled = beta } + UserDefaults.standard.set(true, forKey: "OnboardingCompleted") + openSafariAndQuit() + } + + private func openSafariAndQuit() { SFSafariApplication.showPreferencesForExtension(withIdentifier: extensionBundleIdentifier) { error in DispatchQueue.main.async { - NSApplication.shared.terminate(nil) + if error != nil, let safariURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: "com.apple.Safari") { + NSWorkspace.shared.openApplication(at: safariURL, configuration: NSWorkspace.OpenConfiguration()) + } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + NSApplication.shared.terminate(nil) + } } } } diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 0000000..edd6d56 --- /dev/null +++ b/BUILD.md @@ -0,0 +1,76 @@ +# Building & Releasing AutoPiP + +## Build from Source + +```bash +git clone https://github.com/vordenken/AutoPiP.git +cd AutoPiP +open AutoPiP.xcodeproj +``` + +Then build and run the `AutoPiP` scheme in Xcode. + +## How to Release + +AutoPiP is built and released automatically via GitHub Actions. **`semver.txt`** is the single source of truth for version and changelog. + +### semver.txt Format + +``` +2.1.0 +--- +- First changelog entry +- Second changelog entry + +## 2.0.0 +- Previous release notes +``` + +- **Line 1:** Current version (valid [semver](https://semver.org/)) +- **`---`:** Separator +- **Lines after `---`:** Changelog for the current version (Markdown list items, optional `### ` headings) +- **`## x.y.z`:** Previous release changelogs (preserved history) + +### Workflow + +1. Create a `feature/*` branch from `main` +2. Set the version and changelog in `semver.txt` +3. Push β€” every push to `feature/*` creates a new beta release: + - First push β†’ `v2.1.0-beta1` + - Second push β†’ `v2.1.0-beta2` (auto-incremented from existing tags) + - Beta releases are never overwritten +4. Merge to `main` when ready β€” creates stable release `v2.1.0` + - On `main`, the workflow only runs when `semver.txt` changes + +The workflow automatically: +- Sets the version in the Xcode project and `manifest.json` +- Builds, archives, and creates a DMG +- Signs the DMG with the code signing certificate and Sparkle EdDSA +- Creates a Git tag and GitHub Release with changelog + installation instructions +- Updates `appcast.xml` (with changelog as `<description>`) on the working branch and `main` + +### GitHub App for Branch Protection + +If `main` has branch protection, the workflow needs a GitHub App to push the appcast update. Create a GitHub App with **Contents: Read & Write** permission, install it on the repo, then: +- Add variable `CLIENT_ID` with the App's client ID +- Add secret `APP_PRIVATE_KEY` with the App's private key +- Add the App to the branch protection bypass list + +## Repository Secrets + +| Secret / Variable | Required | Description | +|-------------------|----------|-------------| +| `BUILD_CERTIFICATE_BASE64` | **Yes** | Base64-encoded `.p12` signing certificate | +| `P12_PASSWORD` | **Yes** | Password for the `.p12` file | +| `SPARKLE_PRIVATE_KEY` | **Yes** | EdDSA private key for Sparkle update signatures | +| `CLIENT_ID` (variable) | If branch-protected | GitHub App Client ID for pushing to protected branches | +| `APP_PRIVATE_KEY` | If branch-protected | GitHub App private key | + +**Export your signing certificate:** + +```bash +# Export from Keychain Access as .p12, then encode: +base64 -i certificate.p12 | pbcopy +``` + +**Export your Sparkle key:** The EdDSA private key generated by Sparkle's `generate_keys` tool. Store it as the `SPARKLE_PRIVATE_KEY` secret. diff --git a/README.md b/README.md index 255b3ae..6e9428c 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ <p align="center"> <img src="https://img.shields.io/github/downloads/vordenken/AutoPiP/total" alt="Downloads"> <img src="https://img.shields.io/github/license/vordenken/AutoPiP" alt="License"> + <img src="https://img.shields.io/github/actions/workflow/status/vordenken/AutoPiP/build-release.yml" alt="Build"> <a href="https://makeapullrequest.com"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs Welcome"></a> </p> @@ -25,11 +26,18 @@ A Safari extension that automatically enables Picture-in-Picture (PiP) mode for ## πŸš€ Quick Start -1. Download the latest release [here](https://github.com/vordenken/AutoPiP/releases) -2. Install and enable the Safari extension -3. Start watching videos - PiP activates automatically! +> **⚠️ macOS Gatekeeper:** AutoPiP is not notarized by Apple. macOS will block both the DMG and the app with a security warning β€” this is expected. Follow the steps below to allow them. Once installed, **updates via Sparkle work without this workaround**. -> **Updating:** To receive updates, open the AutoPiP app from time to time. Sparkle checks for updates automatically (once per day). +1. Download the latest `AutoPiP.dmg` from [Releases](https://github.com/vordenken/AutoPiP/releases) +2. Try to open the DMG β€” macOS will block it with *"cannot be opened because it is from an unidentified developer"* + - Open **System Settings β†’ Privacy & Security**, scroll down and click **"Open Anyway"** + - Open the DMG again and drag `AutoPiP.app` to your Applications folder +3. Try to open `AutoPiP.app` β€” macOS will block it again with the same warning + - Open **System Settings β†’ Privacy & Security**, scroll down and click **"Open Anyway"** + - Open the app again β€” it will guide you to enable the Safari extension +4. Enable **AutoPiP** in Safari β†’ Settings β†’ Extensions + +> **Updating:** Open the AutoPiP app occasionally β€” Sparkle checks for updates automatically (once per day). ## 🎯 Compatibility @@ -53,6 +61,10 @@ A Safari extension that automatically enables Picture-in-Picture (PiP) mode for > I wanted to add Chrome/Firefox support but Safari is the only browser that allows calling PiP without user-interaction - So unless this changes, AutoPiP will be Safari only +## πŸ”¨ Building & Releasing + +See [BUILD.md](BUILD.md) for instructions on building from source, releasing, and configuring repository secrets. + ## 🀝 Contributing As this is my first Swift/Xcode project, I welcome: diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..4a4427d --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,18 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +|---------|:---------:| +| 2.x | βœ… | +| < 2.0 | ❌ | + +## Reporting a Vulnerability + +If you discover a security vulnerability in AutoPiP, please report it responsibly: + +1. **Do not** open a public issue +2. Use [GitHub private vulnerability reporting](https://github.com/vordenken/AutoPiP/security/advisories/new) +3. Include a description of the vulnerability and steps to reproduce + +You can expect an initial response within 48 hours. Confirmed vulnerabilities will be patched as soon as possible and credited in the release notes (unless you prefer to remain anonymous). diff --git a/appcast.xml b/appcast.xml index 7a52e57..17ee56a 100755 --- a/appcast.xml +++ b/appcast.xml @@ -2,6 +2,195 @@ <rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle"> <channel> <title>AutoPiP Updates + + Version 2.1.0 (Beta) + beta + + + ⚠️ Beta Release β€” This version may be unstable. Only install if you want to test new features early. +

+
    +

What's New

    +
  • 🎨 New onboarding flow β€” guides through extension setup and update settings on first launch
  • +
  • βš™οΈ Update preferences (auto-check, auto-download, beta channel) now configurable during setup
  • +

Under the Hood

    +
  • πŸ”§ Builds and releases are now fully automated via GitHub Actions
  • +
  • πŸ§ͺ Beta release channel β€” opt in to test upcoming features before they reach the stable release
  • +
+ ]]> +
+ Tue, 16 Jun 2026 20:20:09 +0000 + +
+ + Version 2.1.0 (Beta) + beta + + + ⚠️ Beta Release β€” This version may be unstable. Only install if you want to test new features early. +

+
    +

What's New

    +
  • 🎨 New onboarding flow β€” guides through extension setup and update settings on first launch
  • +
  • βš™οΈ Update preferences (auto-check, auto-download, beta channel) now configurable during setup
  • +

Under the Hood

    +
  • πŸ”§ Builds and releases are now fully automated via GitHub Actions
  • +
  • πŸ§ͺ Beta release channel β€” opt in to test upcoming features before they reach the stable release
  • +
+ ]]> +
+ Tue, 16 Jun 2026 20:16:51 +0000 + +
+ + Version 2.1.0 (Beta) + beta + + + ⚠️ Beta Release β€” This version may be unstable. Only install if you want to test new features early. +

+
    +

What's New

    +
  • 🎨 New onboarding flow β€” guides through extension setup and update settings on first launch
  • +
  • βš™οΈ Update preferences (auto-check, auto-download, beta channel) now configurable during setup
  • +

Under the Hood

    +
  • πŸ”§ Builds and releases are now fully automated via GitHub Actions
  • +
  • πŸ§ͺ Beta release channel β€” opt in to test upcoming features before they reach the stable release
  • +
+ ]]> +
+ Tue, 16 Jun 2026 20:15:10 +0000 + +
+ + Version 2.1.0 (Beta) + beta + + + ⚠️ Beta Release β€” This version may be unstable. Only install if you want to test new features early. +

+
    +

What's New

    +
  • 🎨 New onboarding flow β€” guides through extension setup and update settings on first launch
  • +
  • βš™οΈ Update preferences (auto-check, auto-download, beta channel) now configurable during setup
  • +

Under the Hood

    +
  • πŸ”§ Builds and releases are now fully automated via GitHub Actions
  • +
  • πŸ§ͺ Beta release channel β€” opt in to test upcoming features before they reach the stable release
  • +
+ ]]> +
+ Tue, 16 Jun 2026 20:13:20 +0000 + +
+ + Version 2.1.0 (Beta) + beta + + + ⚠️ Beta Release β€” This version may be unstable. Only install if you want to test new features early. +

+
    +

What's New

    +
  • 🎨 New onboarding flow β€” guides through extension setup and update settings on first launch
  • +
  • βš™οΈ Update preferences (auto-check, auto-download, beta channel) now configurable during setup
  • +

Under the Hood

    +
  • πŸ”§ Builds and releases are now fully automated via GitHub Actions
  • +
  • πŸ§ͺ Beta release channel β€” opt in to test upcoming features before they reach the stable release
  • +
+ ]]> +
+ Tue, 16 Jun 2026 20:11:17 +0000 + +
+ + Version 2.1.0 (Beta) + beta + + + ⚠️ Beta Release β€” This version may be unstable. Only install if you want to test new features early. +

+
    +

What's New

    +
  • 🎨 New onboarding flow β€” guides through extension setup and update settings on first launch
  • +
  • βš™οΈ Update preferences (auto-check, auto-download, beta channel) now configurable during setup
  • +

Under the Hood

    +
  • πŸ”§ Builds and releases are now fully automated via GitHub Actions
  • +
  • πŸ§ͺ Beta release channel β€” opt in to test upcoming features before they reach the stable release
  • +
+ ]]> +
+ Tue, 16 Jun 2026 19:31:11 +0000 + +
+ + Version 2.1.0 (Beta) + beta + + + ⚠️ Beta Release β€” This version may be unstable. Only install if you want to test new features early. +

+
    +

What's New

    +
  • 🎨 New onboarding flow β€” guides through extension setup and update settings on first launch
  • +
  • βš™οΈ Update preferences (auto-check, auto-download, beta channel) now configurable during setup
  • +

Under the Hood

    +
  • πŸ”§ Builds and releases are now fully automated via GitHub Actions
  • +
  • πŸ§ͺ Beta release channel β€” opt in to test upcoming features before they reach the stable release
  • +
+ ]]> +
+ Wed, 15 Apr 2026 11:30:51 +0000 + +
Version 2.0.0 @@ -21,7 +210,8 @@ Fri, 27 Feb 2026 12:00:00 +0000 @@ -40,7 +230,8 @@ Fri, 03 Jan 2026 19:00:00 +0000 @@ -57,7 +248,8 @@ Mo, 21 Jul 2025 12:00:00 +0000 @@ -86,7 +278,8 @@ Fr, 06 Dec 2024 18:17:00 +0000 @@ -105,7 +298,8 @@ Do, 28 Nov 2024 12:45:00 +0000 @@ -122,7 +316,8 @@ Di, 26 Nov 2024 15:00:00 +0000 diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..9995802 --- /dev/null +++ b/renovate.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:recommended"] +} \ No newline at end of file diff --git a/semver.txt b/semver.txt new file mode 100644 index 0000000..c391225 --- /dev/null +++ b/semver.txt @@ -0,0 +1,28 @@ +2.1.0 +--- +### What's New +- 🎨 New onboarding flow β€” guides through extension setup and update settings on first launch +- βš™οΈ Update preferences (auto-check, auto-download, beta channel) now configurable during setup + +### Under the Hood +- πŸ”§ Builds and releases are now fully automated via GitHub Actions +- πŸ§ͺ Beta release channel β€” opt in to test upcoming features before they reach the stable release + +## 2.0.0 +- Global on/off toggle to temporarily disable AutoPiP without removing the extension +- Configurable keyboard shortcut to manually trigger PiP (default: βŒ₯P) +- Blacklist / Whitelist mode to control which sites AutoPiP is active on +- Updated Sparkle to 2.9.0 + +## 1.0.1 +- Fixed issue where clicking into YouTube live chat would incorrectly activate PiP mode +- Improved focus detection to prevent PiP activation on internal page interactions + +## 1.0 +- Bugfix: Twitch.tv + +## 0.4 +- Added Disney+ support +- Added feature for PiP on YouTube scrolling +- Enhanced control over PiP behavior with new settings for tab switching, window switching, and scrolling on YouTube +- Updated popup interface with additional checkboxes for PiP settings