Repository: TheBoredTeam/boring.notch Branch: main Commit: 8fb87eda2468 Files: 183 Total size: 1.5 MB Directory structure: gitextract_x86hf_yn/ ├── .devcontainer/ │ └── devcontainer.json ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── 1-bug-report-form.yml │ │ ├── 1-feature-request-form.yml │ │ ├── config.yml │ │ ├── feature_request.md │ │ └── old_bug_report.md │ ├── PULL_REQUEST.md │ ├── dependabot.yml │ ├── scripts/ │ │ ├── extract_version.py │ │ └── remove_beta.py │ └── workflows/ │ ├── build_reusable.yml │ ├── cicd.yml │ ├── manual_build.yml │ ├── release.yml │ ├── static.yml │ └── update-version-dropdown.yml ├── .gitignore ├── BoringNotchXPCHelper/ │ ├── BoringNotchXPCHelper.entitlements │ ├── BoringNotchXPCHelper.swift │ ├── BoringNotchXPCHelperProtocol.swift │ ├── Info.plist │ └── main.swift ├── CONTRIBUTING.md ├── Configuration/ │ ├── dmg/ │ │ ├── .background/ │ │ │ └── background.tiff │ │ ├── create_dmg.sh │ │ └── dmgbuild_settings.py │ └── sparkle/ │ └── generate_appcast ├── LICENSE ├── README.md ├── SECURITY.md ├── THIRD_PARTY_LICENSES ├── boringNotch/ │ ├── Assets.xcassets/ │ │ ├── AccentColor.colorset/ │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── Github.imageset/ │ │ │ └── Contents.json │ │ ├── bolt.imageset/ │ │ │ └── Contents.json │ │ ├── chrome.imageset/ │ │ │ └── Contents.json │ │ ├── defaultmusic.imageset/ │ │ │ └── Contents.json │ │ ├── logo.imageset/ │ │ │ └── Contents.json │ │ ├── logo2.imageset/ │ │ │ └── Contents.json │ │ ├── plug.imageset/ │ │ │ └── Contents.json │ │ ├── sparkle.imageset/ │ │ │ └── Contents.json │ │ ├── spotlight.imageset/ │ │ │ └── Contents.json │ │ └── theboringteam.imageset/ │ │ └── Contents.json │ ├── BoringViewCoordinator.swift │ ├── ContentView.swift │ ├── Info.plist │ ├── Localizable.xcstrings │ ├── MediaControllers/ │ │ ├── AppleMusicController.swift │ │ ├── MediaControllerProtocol.swift │ │ ├── NowPlayingController.swift │ │ ├── SpotifyController.swift │ │ └── YouTube Music Controller/ │ │ ├── YouTubeMusicAuthentication.swift │ │ ├── YouTubeMusicController.swift │ │ ├── YouTubeMusicModels.swift │ │ └── YouTubeMusicNetworking.swift │ ├── Preview Content/ │ │ └── Preview Assets.xcassets/ │ │ └── Contents.json │ ├── Providers/ │ │ └── CalendarServiceProviding.swift │ ├── Shortcuts/ │ │ └── ShortcutConstants.swift │ ├── XPCHelperClient/ │ │ ├── BoringNotchXPCHelperProtocol.swift │ │ └── XPCHelperClient.swift │ ├── animations/ │ │ ├── HelloAnimation.swift │ │ └── drop.swift │ ├── boring.m4a │ ├── boringNotch.entitlements │ ├── boringNotchApp.swift │ ├── components/ │ │ ├── AnimatedFace.swift │ │ ├── BottomRoundedRectangle.swift │ │ ├── Calendar/ │ │ │ └── BoringCalendar.swift │ │ ├── EmptyState.swift │ │ ├── HoverButton.swift │ │ ├── Live activities/ │ │ │ ├── BoringBattery.swift │ │ │ ├── DownloadView.swift │ │ │ ├── InlineHUD.swift │ │ │ ├── LiveActivityModifier.swift │ │ │ ├── MarqueeTextView.swift │ │ │ ├── OpenNotchHUD.swift │ │ │ └── SystemEventIndicatorModifier.swift │ │ ├── LottieView.swift │ │ ├── Music/ │ │ │ ├── LottieAnimationView.swift │ │ │ └── MusicVisualizer.swift │ │ ├── Notch/ │ │ │ ├── BoringExtrasMenu.swift │ │ │ ├── BoringHeader.swift │ │ │ ├── BoringNotchSkyLightWindow.swift │ │ │ ├── BoringNotchWindow.swift │ │ │ ├── NotchHomeView.swift │ │ │ └── NotchShape.swift │ │ ├── Onboarding/ │ │ │ ├── MusicControllerSelectionView.swift │ │ │ ├── OnboardingFinishView.swift │ │ │ ├── OnboardingView.swift │ │ │ ├── PermissionsRequestView.swift │ │ │ ├── SparkleView.swift │ │ │ └── WelcomeView.swift │ │ ├── ProgressIndicator.swift │ │ ├── Settings/ │ │ │ ├── EditPanelView.swift │ │ │ ├── ListItemPopover.swift │ │ │ ├── MusicSlotConfigurationView.swift │ │ │ ├── SettingsView.swift │ │ │ ├── SettingsWindowController.swift │ │ │ └── SoftwareUpdater.swift │ │ ├── Shelf/ │ │ │ ├── Models/ │ │ │ │ ├── Bookmark.swift │ │ │ │ └── ShelfItem.swift │ │ │ ├── Services/ │ │ │ │ ├── ImageProcessingService.swift │ │ │ │ ├── QuickLookService.swift │ │ │ │ ├── QuickShareService.swift │ │ │ │ ├── ShareServiceFinder.swift │ │ │ │ ├── ShelfActionService.swift │ │ │ │ ├── ShelfDropService.swift │ │ │ │ ├── ShelfPersistenceService.swift │ │ │ │ ├── TemporaryFileStorageService.swift │ │ │ │ └── ThumbnailService.swift │ │ │ ├── ViewModels/ │ │ │ │ ├── ShelfItemViewModel.swift │ │ │ │ ├── ShelfSelectionModel.swift │ │ │ │ └── ShelfStateViewModel.swift │ │ │ └── Views/ │ │ │ ├── DragPreviewView.swift │ │ │ ├── FileShareView.swift │ │ │ ├── ShelfItemView.swift │ │ │ └── ShelfView.swift │ │ ├── Tabs/ │ │ │ ├── TabButton.swift │ │ │ └── TabSelectionView.swift │ │ ├── TestView.swift │ │ ├── Tips/ │ │ │ └── TipStore.swift │ │ ├── Webcam/ │ │ │ └── WebcamView.swift │ │ └── WhatsNewView.swift │ ├── enums/ │ │ └── generic.swift │ ├── extensions/ │ │ ├── ActionBar.swift │ │ ├── BundleInfos.swift │ │ ├── Button+Bouncing.swift │ │ ├── Color+AccentColor.swift │ │ ├── ConditionalModifier.swift │ │ ├── DataTypes+Extensions.swift │ │ ├── KeyboardShortcutsHelper.swift │ │ ├── MouseTracker.swift │ │ ├── NSImage+Extensions.swift │ │ ├── NSItemProvider+LoadHelpers.swift │ │ ├── NSMenu+AssociatedObject.swift │ │ ├── NSScreen+UUID.swift │ │ ├── PanGesture.swift │ │ └── URL+SecurityScoped.swift │ ├── helpers/ │ │ ├── AppIcons.swift │ │ ├── AppleScriptHelper.swift │ │ ├── ApplicationRelauncher.swift │ │ ├── AssociatedObject.swift │ │ ├── AudioPlayer.swift │ │ ├── Clipboard+Content.swift │ │ └── MediaChecker.swift │ ├── managers/ │ │ ├── BatteryActivityManager.swift │ │ ├── BrightnessManager.swift │ │ ├── CalendarManager.swift │ │ ├── ImageService.swift │ │ ├── MusicManager.swift │ │ ├── NotchSpaceManager.swift │ │ ├── VolumeManager.swift │ │ └── WebcamManager.swift │ ├── menu/ │ │ └── StatusBarMenu.swift │ ├── metal/ │ │ └── visualizer.metal │ ├── models/ │ │ ├── BatteryStatusViewModel.swift │ │ ├── BoringViewModel.swift │ │ ├── CalendarModel.swift │ │ ├── Constants.swift │ │ ├── EventModel.swift │ │ ├── MusicControlButton.swift │ │ ├── PlaybackState.swift │ │ └── SharingStateManager.swift │ ├── observers/ │ │ ├── DragDetector.swift │ │ ├── FullscreenMediaDetection.swift │ │ └── MediaKeyInterceptor.swift │ ├── private/ │ │ └── CGSSpace.swift │ ├── sizing/ │ │ └── matters.swift │ └── utils/ │ └── Logger.swift ├── boringNotch.xcodeproj/ │ ├── project.pbxproj │ └── project.xcworkspace/ │ ├── contents.xcworkspacedata │ └── xcshareddata/ │ ├── IDEWorkspaceChecks.plist │ ├── WorkspaceSettings.xcsettings │ └── swiftpm/ │ └── Package.resolved ├── crowdin.yml ├── mediaremote-adapter/ │ ├── MediaRemoteAdapter.framework/ │ │ └── Versions/ │ │ └── A/ │ │ ├── MediaRemoteAdapter │ │ ├── Resources/ │ │ │ └── Info.plist │ │ └── _CodeSignature/ │ │ └── CodeResources │ ├── MediaRemoteAdapterTestClient │ └── mediaremote-adapter.pl └── updater/ └── appcast.xml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .devcontainer/devcontainer.json ================================================ { "image": "mcr.microsoft.com/devcontainers/universal:2", "features": { } } ================================================ FILE: .github/FUNDING.yml ================================================ github: [Alexander5015, iamharshdev] ko-fi: alexander5015 ================================================ FILE: .github/ISSUE_TEMPLATE/1-bug-report-form.yml ================================================ --- name: Bug Report description: File a bug report. title: '[Bug] ' labels: - bug - unconfirmed body: - type: markdown attributes: value: | Thanks for taking the time to fill out this bug report! - type: textarea id: what-happened attributes: label: What happened? What are the steps to reproduce the issue? description: >- Describe the bug and include the steps to replicate the issue. Issues with images or videos will be resolved faster. placeholder: Clearly explain the issue. Please limit each post to one issue. validations: required: true - type: textarea id: expected-behaviour attributes: label: What did you expect to happen? description: A clear and concise description of what you expected to happen. validations: required: true - type: dropdown id: version attributes: label: Boring Notch Version description: >- What version of our software are you running? (Go to ✦ in the menu bar > Settings > About) options: - Select a version - v2.7.3 - v2.7.2 - v2.7.1 - v2.7 - v2.7-rc.3 - v2.6 validations: required: true - type: input id: operating-system attributes: label: macOS Version description: Go to  > About This Mac validations: required: true - type: dropdown id: music-source attributes: label: Music Source? (If relevant) description: >- If this issue is related to music, what music source did you select in Boring Notch settings? options: - Now Playing - Apple Music - Spotify - YouTube Music validations: required: false - type: input id: music-app attributes: label: Music App (If using Now Playing) validations: required: false - type: input id: music-website attributes: label: Website (If using browser for music) validations: required: false - type: textarea id: logs attributes: label: Relevant log output description: >- Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. render: shell - type: checkboxes id: checks attributes: label: Checks description: options: - label: I haven't found any duplicates with my issue. required: true ================================================ FILE: .github/ISSUE_TEMPLATE/1-feature-request-form.yml ================================================ name: Feature Request description: Suggest a new idea or enhancement for Boring Notch title: "[FEATURE] " labels: [] body: - type: markdown attributes: value: | ## Feature Request Thanks for helping make **Boring Notch** better! Please check [existing requests](https://github.com/TheBoredTeam/boring.notch/issues?q=is%3Aissue) before submitting to avoid duplicates. - type: textarea id: problem attributes: label: Is your feature request related to a problem? description: | Please provide a clear description of the problem or pain point this feature would address. **Example:** "I often miss my next meeting because I work in full-screen mode and can't see the system clock or calendar notifications." placeholder: "I'm frustrated when... or It would be great if..." validations: required: true - type: textarea id: solution attributes: label: Describe the solution you'd like description: | How should Boring Notch handle this? Describe the behavior, UI, or interaction you imagine in detail. **Example:** "When a meeting is starting in 5 minutes, the notch could expand slightly to show a countdown timer or the meeting title." placeholder: "I would like the notch to..." validations: required: true - type: textarea id: use-cases attributes: label: Use cases & user scenarios description: | Describe specific scenarios where this feature would be valuable. Who would benefit from this and how often would it be used? **Example:** "Remote workers who attend 5+ video meetings daily would use this constantly. Students could benefit during online classes." placeholder: "This would help users who..." validations: required: false - type: textarea id: additional-context attributes: label: Additional context & screenshots description: | Add any other context, mockups, wireframes, or screenshots about the feature request here. Visuals are incredibly helpful! You can drag and drop images directly into this field. placeholder: "Here is a mockup of how the hover state should look..." validations: required: false - type: checkboxes id: contribution attributes: label: Willingness to contribute description: "We love community contributions! Would you be willing to help build this?" options: - label: "Yes, I can write the code for this feature" - label: "Yes, I can help with design/mockups" - type: checkboxes id: checklist attributes: label: Pre-submission checklist description: "Please confirm the following before submitting:" options: - label: "I have searched existing issues to ensure this isn't a duplicate" required: true - label: "I have provided sufficient detail for the team to understand the request" required: true ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: "[FEATURE]" labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Additional context** Add any other context or screenshots about the feature request here. **Checks** - [x] I haven't found any duplicates with my issue. ================================================ FILE: .github/ISSUE_TEMPLATE/old_bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: "[BUG]" labels: 'bug,unconfirmed' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots or recordings** If applicable, add screenshots to help explain your problem. **Additional context** Add any other context about the problem here. Mentioning what version are you using. **Checks** - [x] I haven't found any duplicates with my issue. ================================================ FILE: .github/PULL_REQUEST.md ================================================ ## Pull Request template Please, go through these steps before you submit a PR. 1. Make sure that your PR is not a duplicate. 2. If not, then make sure that: a. Your changes MUST NOT change translations. Please submit translations on [Crowdin](https://crowdin.com/project/boring-notch). b. You have tested the code yourself to ensure it builds correctly and functions as intended. 3. **After** these steps, you're ready to open a pull request. a. Your pull request MUST NOT target the `main` branch on this repository. You probably want to target `dev` instead. b. Give a descriptive title to your PR. c. Describe your changes. PR should also include screen recording or screenshots to show the changes that were made. d. Put `closes #XXXX` in your description to link your PR to the issue(s) that it fixes (if such). IMPORTANT: Please review the [CONTRIBUTING.md](../CONTRIBUTING.md) file for detailed contributing guidelines. **PLEASE REMOVE THIS TEMPLATE BEFORE SUBMITTING** ================================================ FILE: .github/dependabot.yml ================================================ # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 updates: - package-ecosystem: "github-actions" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "weekly" ================================================ FILE: .github/scripts/extract_version.py ================================================ #!/usr/bin/env python3 from __future__ import annotations import json import os import re import semver import subprocess import sys from argparse import ArgumentParser SEMVER_RE = re.compile(r"v?[0-9]+\.[0-9]+(?:\.[0-9]+)?(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?") def find_first_valid(text: str): for cand in SEMVER_RE.findall(text or ""): s = cand.lstrip("v") # Normalize for parsing: 2.7 -> 2.7.0, 2.7-beta -> 2.7.0-beta # Regex: look for X.Y at start, not followed by .Z normalized = re.sub(r"^([0-9]+\.[0-9]+)(?![0-9]*\.)", r"\1.0", s) try: parsed = semver.VersionInfo.parse(normalized) return s, parsed except Exception: continue return None, None def write_github_output(version: str | None, is_beta_flag: bool) -> None: out = os.environ.get("GITHUB_OUTPUT") if not out: return try: with open(out, "a", encoding="utf-8") as f: f.write(f"version={version or ''}\n") f.write(f"is_beta={str(is_beta_flag).lower()}\n") except Exception: pass def main(argv=None) -> int: p = ArgumentParser() p.add_argument("-c", "--comment", help="Comment body to scan (defaults: $COMMENT or stdin)") args = p.parse_args(argv) comment = args.comment or os.environ.get("COMMENT") if not comment: comment = sys.stdin.read() or "" version, parsed = find_first_valid(comment) beta = getattr(parsed, "prerelease", None) # Write GitHub Actions outputs if available (GITHUB_OUTPUT) write_github_output(version, bool(beta)) # For CLI consumption print simple key=value lines (and a human line) print(f"version={version or ''}") print(f"is_beta={str(bool(beta)).lower()}") print(f"Found version: {version} (beta: {bool(beta)})") return 0 if __name__ == "__main__": raise SystemExit(main()) ================================================ FILE: .github/scripts/remove_beta.py ================================================ #!/usr/bin/env python3 """ Remove the last beta item from an appcast XML file. Usage: remove_beta.py path/to/appcast.xml This script mirrors the inline Python used previously in the workflow. """ import sys import xml.etree.ElementTree as ET from pathlib import Path def remove_last_beta_item(appcast_path: Path) -> int: if not appcast_path.exists(): print(f"Appcast file not found: {appcast_path}") return 1 try: tree = ET.parse(appcast_path) root = tree.getroot() channel = root.find('channel') if channel is None: print('No channel found in appcast') return 0 items = channel.findall('item') removed = False for item in reversed(items): enclosure = item.find('enclosure') if enclosure is not None: version = enclosure.get('sparkle:version', '') if 'beta' in version.lower(): channel.remove(item) removed = True break if removed: tree.write(appcast_path, encoding='utf-8', xml_declaration=True) print('Removed beta item from appcast') else: print('No beta item found in appcast') return 0 except Exception as e: print(f'Error processing appcast: {e}') return 2 if __name__ == '__main__': if len(sys.argv) < 2: print('Usage: remove_beta.py path/to/appcast.xml') sys.exit(1) path = Path(sys.argv[1]) sys.exit(remove_last_beta_item(path)) ================================================ FILE: .github/workflows/build_reusable.yml ================================================ name: "Reusable Build" on: workflow_call: inputs: head_ref: required: true type: string version: required: false type: string build_number: required: false type: string xcode_version: required: false type: string default: "16.4" code_sign_identity: required: false type: string default: "Apple Development" secrets: BUILD_CERTIFICATE_BASE64: required: true P12_PASSWORD: required: true KEYCHAIN_PASSWORD: required: true jobs: build: runs-on: macos-latest permissions: contents: write env: PROJECT_NAME: boringNotch EXPORT_METHOD: development VERSION_INPUT: ${{ inputs.version }} BUILD_NUMBER_INPUT: ${{ inputs.build_number }} DEVELOPMENT_TEAM: ${{ vars.DEVELOPMENT_TEAM_ID }} CODE_SIGN_IDENTITY: ${{ inputs.code_sign_identity }} XCODE_VERSION: ${{ inputs.xcode_version }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: ref: ${{ inputs.head_ref }} - name: Resolve Swift packages run: xcodebuild -resolvePackageDependencies -project ${{ env.PROJECT_NAME }}.xcodeproj - name: Install Apple certificate env: BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} P12_PASSWORD: ${{ secrets.P12_PASSWORD }} KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} run: | CERT_PATH="$RUNNER_TEMP/build_certificate.p12" KC="$RUNNER_TEMP/app-signing.keychain-db" echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode > "$CERT_PATH" security create-keychain -p "$KEYCHAIN_PASSWORD" "$KC" security set-keychain-settings -lut 21600 "$KC" security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KC" security import "$CERT_PATH" -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k "$KC" security list-keychain -d user -s "$KC" - name: Select Xcode ${{ env.XCODE_VERSION }} run: | XCODE_PATH="/Applications/Xcode_${XCODE_VERSION}.app" if [[ ! -d "$XCODE_PATH" ]]; then echo "::error::Xcode ${XCODE_VERSION} not found at ${XCODE_PATH}" echo "Available Xcode versions:" ls -d /Applications/Xcode*.app 2>/dev/null || echo " (none found)" exit 1 fi sudo xcode-select -s "$XCODE_PATH" xcodebuild -version - name: Set version and build number run: | PBXPROJ="${{ env.PROJECT_NAME }}.xcodeproj/project.pbxproj" VERSION="${{ env.VERSION_INPUT }}" BUILD_NUMBER_INPUT="${{ env.BUILD_NUMBER_INPUT }}" if [[ -z "$BUILD_NUMBER_INPUT" ]]; then BUILD_NUMBER="$GITHUB_RUN_NUMBER" else BUILD_NUMBER="$BUILD_NUMBER_INPUT" fi if [[ -n "$VERSION" ]]; then sed -i '' "s/MARKETING_VERSION = [^;]*/MARKETING_VERSION = ${VERSION}/g" "$PBXPROJ" fi sed -i '' "s/CURRENT_PROJECT_VERSION = [^;]*/CURRENT_PROJECT_VERSION = ${BUILD_NUMBER}/g" "$PBXPROJ" - name: Commit version changes run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add "${{ env.PROJECT_NAME }}.xcodeproj/project.pbxproj" VERSION="${{ env.VERSION_INPUT }}" BUILD_NUMBER_INPUT="${{ env.BUILD_NUMBER_INPUT }}" if [[ -z "$BUILD_NUMBER_INPUT" ]]; then BUILD_NUMBER="$GITHUB_RUN_NUMBER" else BUILD_NUMBER="$BUILD_NUMBER_INPUT" fi if [[ -n "$VERSION" ]]; then COMMIT_MSG="Set version to v${VERSION} (build ${BUILD_NUMBER})" else COMMIT_MSG="Set build number to ${BUILD_NUMBER}" fi git commit -m "$COMMIT_MSG" || true git push origin "HEAD:${{ inputs.head_ref }}" || true - name: Build and archive run: | xcodebuild clean archive \ -project ${{ env.PROJECT_NAME }}.xcodeproj \ -scheme ${{ env.PROJECT_NAME }} \ -archivePath ${{ env.PROJECT_NAME }} \ -destination "generic/platform=macOS" \ DEVELOPMENT_TEAM="$DEVELOPMENT_TEAM" \ CODE_SIGN_IDENTITY="$CODE_SIGN_IDENTITY" \ ONLY_ACTIVE_ARCH=NO \ -allowProvisioningUpdates - name: Export app run: | cat > "$RUNNER_TEMP/export_options.plist" < method ${{ env.EXPORT_METHOD }} signingStyle automatic teamID ${DEVELOPMENT_TEAM} PLIST xcodebuild -exportArchive \ -archivePath "${{ env.PROJECT_NAME }}.xcarchive" \ -exportPath Release \ -exportOptionsPlist "$RUNNER_TEMP/export_options.plist" - name: Verify generate_appcast exists run: | if [ ! -x Configuration/sparkle/generate_appcast ]; then echo "::warning title=Missing generate_appcast::Configuration/sparkle/generate_appcast is not present or not executable; skipping appcast generation verification." fi - name: Create DMG run: | VENV_DIR="$RUNNER_TEMP/venv" python3 -m venv "$VENV_DIR" source "$VENV_DIR/bin/activate" pip install --upgrade pip setuptools wheel "dmgbuild[badge_icons]" chmod +x Configuration/dmg/create_dmg.sh ./Configuration/dmg/create_dmg.sh \ "Release/${{ env.PROJECT_NAME }}.app" \ "Release/${{ env.PROJECT_NAME }}.dmg" \ "${{ env.PROJECT_NAME }} ${{ env.VERSION_INPUT }}" - name: Upload DMG uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f with: name: ${{ env.PROJECT_NAME }}.dmg path: Release/${{ env.PROJECT_NAME }}.dmg - name: Upload .app uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f with: name: ${{ env.PROJECT_NAME }}.app path: Release/${{ env.PROJECT_NAME }}.app ================================================ FILE: .github/workflows/cicd.yml ================================================ name: Build for macOS on: push: branches: - '*' pull_request: branches: - '*' # https://stackoverflow.com/a/72408109/6942800 concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true # Least-privilege: only needs to read code for building. permissions: contents: read jobs: build: name: Build Boring Notch strategy: matrix: platform: - macOS xcode: - ^16 scheme: - boringNotch runs-on: macos-latest steps: - name: Code Checkout # TODO: pin to immutable SHA uses: actions/checkout@v6.0.2 - # TODO: pin to immutable SHA uses: mxcl/xcodebuild@v3 with: xcode: ${{ matrix.xcode }} platform: ${{ matrix.platform }} scheme: ${{ matrix.scheme }} action: build verbosity: xcpretty upload-logs: always configuration: release ================================================ FILE: .github/workflows/manual_build.yml ================================================ name: "Manual Build" on: workflow_dispatch: inputs: head_ref: description: 'Branch to build' required: false default: main version: description: 'Marketing version (optional)' required: false build_number: description: 'Build number (optional)' required: false xcode_version: description: 'Xcode version (optional)' required: false default: '16.4' # Least-privilege: workflow_dispatch build only; no token access needed beyond artifacts. permissions: contents: write jobs: build: name: Build and sign app uses: ./.github/workflows/build_reusable.yml with: head_ref: ${{ github.event.inputs.head_ref || github.ref_name || 'main' }} version: ${{ github.event.inputs.version || '' }} build_number: ${{ github.event.inputs.build_number || '' }} xcode_version: ${{ github.event.inputs.xcode_version || '16.4' }} code_sign_identity: "Apple Development" secrets: BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} P12_PASSWORD: ${{ secrets.P12_PASSWORD }} KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} ================================================ FILE: .github/workflows/release.yml ================================================ name: "Deploy Boring Notch" on: issue_comment: types: [created] concurrency: group: release-${{ github.event.issue.number || github.run_id }} cancel-in-progress: true env: PROJECT_NAME: boringNotch BETA_CHANNEL_NAME: beta RELEASE_COMMAND: /release CODE_SIGN_IDENTITY: "Apple Development" XCODE_VERSION: "16.4" permissions: contents: read pull-requests: write jobs: # helper job to test for the release command in the comment; env is safe to use here check_release: name: Check for release command runs-on: ubuntu-latest outputs: is_release: ${{ steps.check.outputs.is_release }} steps: - id: check run: | if [[ "${{ github.event.comment.body }}" == *"${{ env.RELEASE_COMMAND }}"* ]]; then echo "is_release=true" >> $GITHUB_OUTPUT else echo "is_release=false" >> $GITHUB_OUTPUT fi preparation: name: Preparation if: ${{ github.event.issue.pull_request && needs.check_release.outputs.is_release == 'true' }} runs-on: ubuntu-latest permissions: contents: write pull-requests: write outputs: is_beta: ${{ steps.extract_version.outputs.is_beta }} version: ${{ steps.extract_version.outputs.version }} build_number: ${{ steps.extract_version.outputs.build_number }} title: ${{ steps.release_notes.outputs.title }} release_notes: ${{ steps.release_notes.outputs.release_notes }} release_notes_github: ${{ steps.release_notes.outputs.release_notes_github }} head_ref: ${{ steps.pr_info.outputs.head_ref }} base_ref: ${{ steps.pr_info.outputs.base_ref }} steps: - name: Validate permissions and PR state env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail REPO="${{ github.repository }}" COMMENTER="${{ github.event.comment.user.login }}" PR_NUMBER="${{ github.event.issue.number }}" # Acknowledge the command gh api "repos/${REPO}/issues/comments/${{ github.event.comment.id }}/reactions" \ -f content=eyes --silent # Require admin permission PERM=$(gh api "repos/${REPO}/collaborators/${COMMENTER}/permission" --jq '.permission') if [[ "$PERM" != "admin" ]]; then echo "::error::${COMMENTER} is not an admin (permission: ${PERM})" exit 1 fi # Require mergeable, non-draft PR IS_DRAFT=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.draft') MERGEABLE=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.mergeable') if [[ "$IS_DRAFT" == "true" || "$MERGEABLE" != "true" ]]; then echo "::error::PR #${PR_NUMBER} is not ready to merge (draft=${IS_DRAFT}, mergeable=${MERGEABLE})" exit 1 fi - name: Get PR branch info id: pr_info env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail PR_DATA=$(gh api "repos/${{ github.repository }}/pulls/${{ github.event.issue.number }}" \ --jq '{head_ref: .head.ref, base_ref: .base.ref}') echo "head_ref=$(echo "$PR_DATA" | jq -r '.head_ref')" >> "$GITHUB_OUTPUT" echo "base_ref=$(echo "$PR_DATA" | jq -r '.base_ref')" >> "$GITHUB_OUTPUT" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ steps.pr_info.outputs.head_ref }} - name: Ensure scripts directory exists env: BASE_REF: ${{ steps.pr_info.outputs.base_ref }} run: | if [ ! -f ".github/scripts/extract_version.py" ]; then echo "Script not found in PR branch, fetching from base branch" git fetch origin "$BASE_REF" git checkout "origin/$BASE_REF" -- .github/scripts/ fi - name: Extract version from comment id: extract_version env: COMMENT: ${{ github.event.comment.body }} run: | set -euo pipefail python3 -m pip install --upgrade --no-cache-dir semver export projname="${{ env.PROJECT_NAME }}" OUTPUT=$(python3 .github/scripts/extract_version.py -c "$COMMENT") VERSION=$(awk -F= '/^version=/{print $2; exit}' <<<"$OUTPUT") IS_BETA=$(awk -F= '/^is_beta=/{print $2; exit}' <<<"$OUTPUT") BUILD_NUMBER="${GITHUB_RUN_NUMBER}" echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "is_beta=$IS_BETA" >> "$GITHUB_OUTPUT" echo "build_number=$BUILD_NUMBER" >> "$GITHUB_OUTPUT" echo "Version: $VERSION | Beta: $IS_BETA | Build: $BUILD_NUMBER" - name: Generate release notes from PR id: release_notes env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail PR_NUMBER="${{ github.event.issue.number }}" REPO="${{ github.repository }}" TITLE=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.title // "Release"') BODY=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.body // "- No release notes provided"') # Strip H1 headings for GitHub release notes BODY_GITHUB=$(printf '%s' "$BODY" | sed '/^# /d' | perl -pe 's#]*>.*?##gi') { echo "title=${TITLE}" echo "release_notes<> "$GITHUB_OUTPUT" - name: Check version not already released env: VERSION: ${{ steps.extract_version.outputs.version }} run: | git fetch --tags if git rev-parse -q --verify "refs/tags/v${VERSION}" >/dev/null; then echo "::error::Version v${VERSION} already exists as a tag" exit 1 fi - name: Sync branch (stable releases only) if: steps.extract_version.outputs.is_beta == 'false' env: GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} BASE_REF: ${{ steps.pr_info.outputs.base_ref }} HEAD_REF: ${{ steps.pr_info.outputs.head_ref }} run: | set -euo pipefail git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git config --global credential.https://github.com.helper \ '!f() { echo "username=x-access-token"; printf "password=%s\n" "${GITHUB_TOKEN}"; }; f' git fetch origin "$BASE_REF" git checkout "$HEAD_REF" git merge --no-ff "origin/$BASE_REF" -m "Sync branch before release" git push origin "$HEAD_REF" build: name: Build and sign needs: preparation permissions: contents: write uses: ./.github/workflows/build_reusable.yml with: head_ref: ${{ needs.preparation.outputs.head_ref }} version: ${{ needs.preparation.outputs.version }} build_number: ${{ needs.preparation.outputs.build_number }} xcode_version: ${{ needs.preparation.outputs.xcode_version }} code_sign_identity: ${{ needs.preparation.outputs.code_sign_identity }} secrets: BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} P12_PASSWORD: ${{ secrets.P12_PASSWORD }} KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} publish: name: Publish release needs: [preparation, build] runs-on: macos-latest permissions: contents: write env: HEAD_REF: ${{ needs.preparation.outputs.head_ref }} VERSION: ${{ needs.preparation.outputs.version }} IS_BETA: ${{ needs.preparation.outputs.is_beta }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ env.HEAD_REF }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ env.PROJECT_NAME }}.dmg path: Release - name: Create embedded release notes env: RELEASE_NOTES: ${{ needs.preparation.outputs.release_notes }} run: printf '%s' "$RELEASE_NOTES" > Release/boringNotch.html - name: Generate signed appcast env: SPARKLE_PRIVATE_KEY: ${{ secrets.PRIVATE_SPARKLE_KEY }} run: | set -euo pipefail test -x Configuration/sparkle/generate_appcast || { echo "::error::Configuration/sparkle/generate_appcast missing or not executable"; exit 1; } CHANNEL_ARGS=() if [[ "${IS_BETA}" == "true" ]]; then CHANNEL_ARGS=(--channel "${{ env.BETA_CHANNEL_NAME }}") fi printf '%s' "$SPARKLE_PRIVATE_KEY" | ./Configuration/sparkle/generate_appcast \ --ed-key-file - \ --link "https://github.com/TheBoredTeam/boring.notch/releases" \ --download-url-prefix "https://github.com/TheBoredTeam/boring.notch/releases/download/v${VERSION}/" \ --embed-release-notes \ "${CHANNEL_ARGS[@]}" \ -o updater/appcast.xml \ Release/ - name: Commit appcast run: | set -euo pipefail git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" if [[ "${IS_BETA}" == "false" ]]; then git add updater/appcast.xml git commit -m "Update version to v${VERSION} and appcast" || true git push origin "HEAD:${HEAD_REF}" || true else # Save generated appcast, switch to main, apply and push cp updater/appcast.xml "$RUNNER_TEMP/appcast.xml" git fetch origin main git checkout main cp "$RUNNER_TEMP/appcast.xml" updater/appcast.xml git add updater/appcast.xml git commit -m "Update appcast with beta release for v${VERSION}" || true git push origin main fi - name: Create GitHub release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} TITLE: ${{ needs.preparation.outputs.title }} NOTES: ${{ needs.preparation.outputs.release_notes_github }} run: | RELEASE_FLAGS=() if [[ "${IS_BETA}" == "true" ]]; then RELEASE_FLAGS=(--prerelease) fi gh release create "v${VERSION}" Release/boringNotch.dmg \ --title "v${VERSION} - ${TITLE}" \ --notes "$NOTES" \ "${RELEASE_FLAGS[@]}" upgrade-brew: name: Update Homebrew cask needs: [preparation, publish] runs-on: ubuntu-latest env: VERSION: ${{ needs.preparation.outputs.version }} IS_BETA: ${{ needs.preparation.outputs.is_beta }} steps: - name: Generate cask files run: | set -euo pipefail DMG_URL="https://github.com/TheBoredTeam/boring.notch/releases/download/v${VERSION}/boringNotch.dmg" # Retry SHA calculation (release may need a moment to propagate) for attempt in 1 2 3; do if SHA256=$(curl -sL --fail "$DMG_URL" | shasum -a 256 | cut -d' ' -f1); then break; fi echo "Attempt $attempt failed, retrying in 10s..."; sleep 10 done [[ -n "${SHA256:-}" ]] || { echo "::error::Failed to download DMG for SHA256"; exit 1; } write_cask() { local CASK_NAME="$1" DISPLAY_NAME="$2" DESC="$3" cat <= :sonoma" app "boringNotch.app" postflight do app_path = appdir/"boringNotch.app" next unless app_path.exist? system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", app_path] end uninstall quit: "theboringteam.boringnotch" zap trash: [ "~/Library/Application Scripts/theboringteam.boringnotch/", "~/Library/Containers/theboringteam.boringnotch/", ] end CASK } write_cask "boring-notch@rc" "Boring Notch RC" \ "Not so boring notch That Rocks (Release Candidate)" > boring-notch@rc.rb if [[ "${IS_BETA}" == "false" ]]; then write_cask "boring-notch" "Boring Notch" \ "Not so boring notch That Rocks" > boring-notch.rb fi - name: Upload cask artifacts uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: homebrew-cask-${{ env.VERSION }} path: boring-notch*.rb - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: repository: TheBoredTeam/homebrew-boring-notch token: ${{ secrets.HOMEBREW_TAP_TOKEN }} path: homebrew-tap - name: Update casks in tap run: | set -euo pipefail cp boring-notch@rc.rb homebrew-tap/Casks/boring-notch@rc.rb COMMIT_MSG="Update boring-notch@rc to v${VERSION}" if [[ "${IS_BETA}" == "false" ]]; then cp boring-notch.rb homebrew-tap/Casks/boring-notch.rb COMMIT_MSG="Update boring-notch and boring-notch@rc to v${VERSION}" fi cd homebrew-tap git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add Casks/ if git diff --cached --quiet; then echo "No changes to commit" else git commit -m "$COMMIT_MSG" git push fi ending: name: Finalize if: ${{ always() && needs.preparation.result != 'skipped' && needs.check_release.outputs.is_release == 'true' }} needs: [check_release, preparation, build, publish, upgrade-brew] runs-on: ubuntu-latest permissions: contents: write env: ALL_RESULTS: ${{ join(needs.*.result, ',') }} RELEASE_SUCCEEDED: ${{ !contains(join(needs.*.result, ','), 'failure') && !contains(join(needs.*.result, ','), 'cancelled') }} steps: - name: React to trigger comment env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | REPO="${{ github.repository }}" COMMENT_ID="${{ github.event.comment.id }}" if [[ "$ALL_RESULTS" != *"failure"* && "$ALL_RESULTS" != *"cancelled"* ]]; then REACTION="rocket" else REACTION="confused" fi gh api "repos/${REPO}/issues/comments/${COMMENT_ID}/reactions" \ -f content="$REACTION" --silent - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 if: needs.preparation.outputs.is_beta == 'false' && env.RELEASE_SUCCEEDED == 'true' with: ref: ${{ needs.preparation.outputs.head_ref }} fetch-depth: 0 - name: Merge PR (stable releases only) if: needs.preparation.outputs.is_beta == 'false' && env.RELEASE_SUCCEEDED == 'true' env: GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} HEAD_REF: ${{ needs.preparation.outputs.head_ref }} BASE_REF: ${{ needs.preparation.outputs.base_ref }} VERSION: ${{ needs.preparation.outputs.version }} run: | set -euo pipefail git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git config --global credential.https://github.com.helper \ '!f() { echo "username=x-access-token"; printf "password=%s\n" "${GITHUB_TOKEN}"; }; f' git fetch origin "$BASE_REF" git checkout "$BASE_REF" git merge --no-ff "origin/$HEAD_REF" -m "Release version v${VERSION}" git push origin "$BASE_REF" - name: Summary env: IS_BETA: ${{ needs.preparation.outputs.is_beta }} VERSION: ${{ needs.preparation.outputs.version }} BUILD_NUMBER: ${{ needs.preparation.outputs.build_number }} shell: bash run: | if [[ "${IS_BETA}" == "true" ]]; then BUILD_TYPE="beta" else BUILD_TYPE="stable" fi if [[ "${RELEASE_SUCCEEDED}" == "true" ]]; then echo "✅ Released boringNotch v${VERSION} (${BUILD_TYPE} build ${BUILD_NUMBER})" >> "$GITHUB_STEP_SUMMARY" echo "🍺 Homebrew cask updated" >> "$GITHUB_STEP_SUMMARY" echo "📱 Sparkle appcast updated" >> "$GITHUB_STEP_SUMMARY" else echo "❌ Release failed${VERSION:+ for boringNotch v${VERSION}}" >> "$GITHUB_STEP_SUMMARY" fi ================================================ FILE: .github/workflows/static.yml ================================================ # Simple workflow for deploying static content to GitHub Pages name: Deploy static content to Pages on: # Runs on pushes targeting the default branch push: branches: - main # Allows you to run this workflow manually from the Actions tab workflow_dispatch: # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: contents: read pages: write id-token: write # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. concurrency: group: "pages" cancel-in-progress: false jobs: # Single deploy job since we're just deploying deploy: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Pages uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5 - name: Upload artifact uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0 with: # Upload entire repository path: 'updater/' - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 ================================================ FILE: .github/workflows/update-version-dropdown.yml ================================================ name: Update Version Dropdown in Issue Form # Least-privilege: only needs to push the updated issue template file. permissions: contents: write on: push: tags: - '*' workflow_dispatch: {} release: types: - published jobs: update-dropdown: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event.repository.default_branch }} fetch-depth: 0 persist-credentials: true - name: Fetch tags run: git fetch --tags - name: Get latest 5 versions with placeholder id: tags run: | TAGS=$(git tag --sort=-v:refname | head -n 5 | jq -R -s -c 'split("\n")[:-1]') VERSIONS=$(echo '["Select a version"]' | jq -c --argjson tags "$TAGS" '. + $tags') echo "versions=$VERSIONS" >> $GITHUB_OUTPUT - name: Issue Forms Dropdown Options uses: ShaMan123/gha-form-dropdown-options@124b08a39f06a6a4fcafdec501ecaca8db019db2 # v2.1.0 with: form: .github/ISSUE_TEMPLATE/1-bug-report-form.yml dropdown: version options: ${{ steps.tags.outputs.versions }} - name: Commit changes run: | git config user.name "GitHub Actions Bot" git config user.email "actions@github.com" if git diff --quiet; then echo "No changes to commit." else git add .github/ISSUE_TEMPLATE/1-bug-report-form.yml git commit -m "Update issue form version dropdown with placeholder and latest tags" git push fi ================================================ FILE: .gitignore ================================================ # Xcode *.xcuserstate *.xcuserdata *.xcscheme *.xcuserdatad *.pbxuser *.xccheckout *.xcscheme *.xcplayground *.xcuserdatad *.xctest *.xcuserdata *.xcodeproj/xcshareddata/WorkspaceSettings.xcsettings # CocoaPods Pods/ podfile.lock # Carthage Carthage/Build/ # Swift Package Manager .swiftpm/ .build/ # Derived data DerivedData/ # Build output build/ *.app *.dSYM # Temporary files *.swp *.swo *.tmp *.log # Other *.DS_Store *.vscode/ *.idea/ # User-specific files *.xcuserdatad/ *.xcscheme *.xcodeproj/xcuserdata *.xcodeproj/project.xcuserdata # Build artifacts *.ipa *.xcarchive *.dSYM ================================================ FILE: BoringNotchXPCHelper/BoringNotchXPCHelper.entitlements ================================================ com.apple.security.app-sandbox ================================================ FILE: BoringNotchXPCHelper/BoringNotchXPCHelper.swift ================================================ // // BoringNotchXPCHelper.swift // BoringNotchXPCHelper // // Created by Alexander on 2025-11-16. // import Foundation import ApplicationServices import IOKit import CoreGraphics class BoringNotchXPCHelper: NSObject, BoringNotchXPCHelperProtocol { @objc func isAccessibilityAuthorized(with reply: @escaping (Bool) -> Void) { reply(AXIsProcessTrusted()) } @objc func requestAccessibilityAuthorization() { let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary AXIsProcessTrustedWithOptions(options) } @objc func ensureAccessibilityAuthorization(_ promptIfNeeded: Bool, with reply: @escaping (Bool) -> Void) { if AXIsProcessTrusted() { reply(true) return } if promptIfNeeded { requestAccessibilityAuthorization() } DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { reply(AXIsProcessTrusted()) } } private class KeyboardBrightnessClient { private static let keyboardID: UInt64 = 1 private var clientInstance: NSObject? private let getSelector = NSSelectorFromString("brightnessForKeyboard:") private let setSelector = NSSelectorFromString("setBrightness:forKeyboard:") init() { var loaded = false let bundlePaths = [ "/System/Library/PrivateFrameworks/CoreBrightness.framework", "/System/Library/PrivateFrameworks/CoreBrightness.framework/CoreBrightness" ] for path in bundlePaths where !loaded { if let bundle = Bundle(path: path) { loaded = bundle.load() } } if loaded, let cls = NSClassFromString("KeyboardBrightnessClient") as? NSObject.Type { clientInstance = cls.init() } } var isAvailable: Bool { clientInstance != nil } func currentBrightness() -> Float? { guard let clientInstance, let fn: BrightnessGetter = methodIMP(on: clientInstance, selector: getSelector, as: BrightnessGetter.self) else { return nil } return fn(clientInstance, getSelector, Self.keyboardID) } func setBrightness(_ value: Float) -> Bool { guard let clientInstance, let fn: BrightnessSetter = methodIMP(on: clientInstance, selector: setSelector, as: BrightnessSetter.self) else { return false } return fn(clientInstance, setSelector, value, Self.keyboardID).boolValue } private typealias BrightnessGetter = @convention(c) (NSObject, Selector, UInt64) -> Float private typealias BrightnessSetter = @convention(c) (NSObject, Selector, Float, UInt64) -> ObjCBool private func methodIMP(on object: NSObject, selector: Selector, as type: T.Type) -> T? { guard let cls = object_getClass(object), let method = class_getInstanceMethod(cls, selector) else { return nil } let imp = method_getImplementation(method) return unsafeBitCast(imp, to: type) } } private static let keyboardClient = KeyboardBrightnessClient() @objc func isKeyboardBrightnessAvailable(with reply: @escaping (Bool) -> Void) { reply(Self.keyboardClient.isAvailable) } @objc func currentKeyboardBrightness(with reply: @escaping (NSNumber?) -> Void) { reply(Self.keyboardClient.currentBrightness().map { NSNumber(value: $0) }) } @objc func setKeyboardBrightness(_ value: Float, with reply: @escaping (Bool) -> Void) { reply(Self.keyboardClient.setBrightness(value)) } // MARK: - Screen Brightness (moved from client app into helper) @objc func isScreenBrightnessAvailable(with reply: @escaping (Bool) -> Void) { var b: Float = 0 reply(displayServicesGetBrightness(displayID: CGMainDisplayID(), out: &b) || ioServiceFor(displayID: CGMainDisplayID()) != nil) } @objc func currentScreenBrightness(with reply: @escaping (NSNumber?) -> Void) { var b: Float = 0 if displayServicesGetBrightness(displayID: CGMainDisplayID(), out: &b) { reply(NSNumber(value: b)) return } if let io = ioServiceFor(displayID: CGMainDisplayID()) { var level: Float = 0 if IODisplayGetFloatParameter(io, 0, kIODisplayBrightnessKey as CFString, &level) == kIOReturnSuccess { IOObjectRelease(io) reply(NSNumber(value: level)) return } IOObjectRelease(io) } reply(nil) } @objc func setScreenBrightness(_ value: Float, with reply: @escaping (Bool) -> Void) { let clamped = max(0, min(1, value)) if displayServicesSetBrightness(displayID: CGMainDisplayID(), value: clamped) { reply(true) return } if let io = ioServiceFor(displayID: CGMainDisplayID()) { let ok = IODisplaySetFloatParameter(io, 0, kIODisplayBrightnessKey as CFString, clamped) == kIOReturnSuccess IOObjectRelease(io) reply(ok) return } reply(false) } // MARK: - Private helpers for DisplayServices / IOKit access private func displayServicesGetBrightness(displayID: CGDirectDisplayID, out: inout Float) -> Bool { guard let sym = dlsym(DisplayServicesHandle.handle, "DisplayServicesGetBrightness") else { return false } typealias Fn = @convention(c) (CGDirectDisplayID, UnsafeMutablePointer) -> Int32 let fn = unsafeBitCast(sym, to: Fn.self) var tmp: Float = 0 let r = fn(displayID, &tmp) if r == 0 { out = tmp; return true } return false } private func displayServicesSetBrightness(displayID: CGDirectDisplayID, value: Float) -> Bool { guard let sym = dlsym(DisplayServicesHandle.handle, "DisplayServicesSetBrightness") else { return false } typealias Fn = @convention(c) (CGDirectDisplayID, Float) -> Int32 let fn = unsafeBitCast(sym, to: Fn.self) return fn(displayID, value) == 0 } private func ioServiceFor(displayID: CGDirectDisplayID) -> io_service_t? { var iterator: io_iterator_t = 0 guard IOServiceGetMatchingServices(kIOMainPortDefault, IOServiceMatching("IODisplayConnect"), &iterator) == kIOReturnSuccess else { return nil } defer { IOObjectRelease(iterator) } while case let service = IOIteratorNext(iterator), service != 0 { let info = IODisplayCreateInfoDictionary(service, 0).takeRetainedValue() as NSDictionary if let vendorID = info[kDisplayVendorID] as? UInt32, let productID = info[kDisplayProductID] as? UInt32, vendorID == CGDisplayVendorNumber(displayID), productID == CGDisplayModelNumber(displayID) { return service } IOObjectRelease(service) } return nil } // MARK: - Helper handle for private framework private enum DisplayServicesHandle { static let handle: UnsafeMutableRawPointer? = { let paths = [ "/System/Library/PrivateFrameworks/DisplayServices.framework/DisplayServices", "/System/Library/PrivateFrameworks/DisplayServices.framework/Versions/Current/DisplayServices" ] for p in paths { if let h = dlopen(p, RTLD_LAZY) { return h } } return nil }() } } ================================================ FILE: BoringNotchXPCHelper/BoringNotchXPCHelperProtocol.swift ================================================ // // BoringNotchXPCHelperProtocol.swift // BoringNotchXPCHelper // // Created by Alexander on 2025-11-16. // import Foundation /// The protocol that this service will vend as its API. This protocol will also need to be visible to the process hosting the service. @objc protocol BoringNotchXPCHelperProtocol { func isAccessibilityAuthorized(with reply: @escaping (Bool) -> Void) func requestAccessibilityAuthorization() func ensureAccessibilityAuthorization(_ promptIfNeeded: Bool, with reply: @escaping (Bool) -> Void) // Keyboard backlight / CoreBrightness access (performed by the helper) func isKeyboardBrightnessAvailable(with reply: @escaping (Bool) -> Void) func currentKeyboardBrightness(with reply: @escaping (NSNumber?) -> Void) func setKeyboardBrightness(_ value: Float, with reply: @escaping (Bool) -> Void) // Screen brightness access (performed by the helper) func isScreenBrightnessAvailable(with reply: @escaping (Bool) -> Void) func currentScreenBrightness(with reply: @escaping (NSNumber?) -> Void) func setScreenBrightness(_ value: Float, with reply: @escaping (Bool) -> Void) } /* To use the service from an application or other process, use NSXPCConnection to establish a connection to the service by doing something like this: connectionToService = NSXPCConnection(serviceName: "theboringteam.boringnotch.BoringNotchXPCHelper") connectionToService.remoteObjectInterface = NSXPCInterface(with: (any BoringNotchXPCHelperProtocol).self) connectionToService.resume() Once you have a connection to the service, you can use it like this: if let proxy = connectionToService.remoteObjectProxy as? BoringNotchXPCHelperProtocol { proxy.performCalculation(firstNumber: 23, secondNumber: 19) { result in NSLog("Result of calculation is: \(result)") } } And, when you are finished with the service, clean up the connection like this: connectionToService.invalidate() */ ================================================ FILE: BoringNotchXPCHelper/Info.plist ================================================ XPCService ServiceType Application ================================================ FILE: BoringNotchXPCHelper/main.swift ================================================ // // main.swift // BoringNotchXPCHelper // // Created by Alexander on 2025-11-16. // import Foundation class ServiceDelegate: NSObject, NSXPCListenerDelegate { /// This method is where the NSXPCListener configures, accepts, and resumes a new incoming NSXPCConnection. func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool { // Configure the connection. // First, set the interface that the exported object implements. newConnection.exportedInterface = NSXPCInterface(with: (any BoringNotchXPCHelperProtocol).self) // Next, set the object that the connection exports. All messages sent on the connection to this service will be sent to the exported object to handle. The connection retains the exported object. let exportedObject = BoringNotchXPCHelper() newConnection.exportedObject = exportedObject // Resuming the connection allows the system to deliver more incoming messages. newConnection.resume() // Returning true from this method tells the system that you have accepted this connection. If you want to reject the connection for some reason, call invalidate() on the connection and return false. return true } } // Create the delegate for the service. let delegate = ServiceDelegate() // Set up the one NSXPCListener for this service. It will handle all incoming connections. let listener = NSXPCListener.service() listener.delegate = delegate // Resuming the serviceListener starts this service. This method does not return. listener.resume() ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Thank you for taking the time to contribute! ❤️ These guidelines help streamline the contribution process for everyone involved. By following them, you'll make it easier for maintainers to review your work and collaborate with you effectively. You can contribute in many ways: writing code, improving documentation, reporting bugs, requesting features, or creating tutorials and blog posts. Every contribution, large or small, helps make Boring Notch better. ## Table of Contents - [Localizations](#localizations) - [Contributing Code](#contributing-code) - [Before You Start](#before-you-start) - [Setting Up Your Environment](#setting-up-your-environment) - [Making Changes](#making-changes) - [Pull Requests](#pull-requests) - [Reporting Bugs](#reporting-bugs) - [Feature Requests](#feature-requests) - [Getting Help](#getting-help) ## Localizations Please submit all translations to [Crowdin](https://crowdin.com/project/boring-notch). New strings added to the `dev` branch from code changes will sync automatically to Crowdin, and Crowdin will automatically open a new PR with translations to allow us to integrate them. ## Contributing Code ### Before You Start - **Check existing issues**: Before creating a new issue or starting work, search existing issues to avoid duplicates. - **Discuss major changes**: For significant features or major changes, please open an issue first to discuss your approach with maintainers and the community. > [!IMPORTANT] > All code contributions must be based on the `dev` branch, not `main`. Documentation changes should be based on `main` instead. ### Setting Up Your Environment 1. **Fork the repository**: Click the "Fork" button at the top of the repository page to create your own copy. 2. **Clone your fork**: ```bash git clone https://github.com/{your-username}/boring.notch.git cd boring.notch ``` Replace `{your-username}` with your GitHub username. 3. **Switch to the `dev` branch**: ```bash git checkout dev ``` All code contributions must be based on the `dev` branch, not `main`. Documentation changes should be based on `main` instead. 5. **Create a new feature branch**: ```bash git checkout -b feature/{your-feature-name} ``` Replace `{your-feature-name}` with a descriptive name. Use lowercase letters, numbers, and hyphens only (e.g., `feature/add-dark-mode` or `fix/notification-crash`). ### Making Changes 1. **Make your changes**: Implement your feature or bug fix. Write clean, well-documented code 2. **Test your changes**: Ensure your changes work as expected and don't break existing functionality. 3. **Commit your changes**: ```bash git add . git commit -m "Add descriptive commit message" ``` Write clear, concise commit messages that explain what your changes do and why. 4. **Keep your branch up to date**: Regularly sync your branch with the latest changes from the `dev` branch to avoid conflicts. 5. **Push to your fork**: ```bash git push origin feature/{your-feature-name} ``` ### Pull Requests 1. **Create a pull request**: Go to the original repository and click "New Pull Request." Select your feature branch and set the base branch to `dev`. 2. **Write a detailed description**: Your PR should include: - A clear title summarizing the changes - A detailed description of what was changed and why - Reference to any related issues (e.g., "Fixes #123" or "Relates to #456") - Screenshots or screen recordings for UI changes 3. **Respond to feedback**: Maintainers may request changes. 4. **Be patient**: Reviews take time. Maintainers will get to your PR as soon as they can. ## Reporting Bugs When reporting bugs, please include: - A clear, descriptive title - Steps to reproduce the issue - Expected behavior vs. actual behavior - Screenshots or error messages if applicable - Your environment details (OS version, app version, etc.) ## Feature Requests Feature requests are welcome! Please: - Check if the feature has already been requested - Clearly describe the feature and its use case - Explain why this feature would be valuable to users - Be open to discussion and alternative approaches ## Getting Help If you need help or have questions: - Check the project documentation - Search existing issues for similar questions - Open a new issue with the "question" label - Join our [community Discord server](https://discord.com/servers/boring-notch-1269588937320566815) --- Thank you for contributing to Boring Notch! Your efforts help make this project better for everyone. 🎉 ================================================ FILE: Configuration/dmg/create_dmg.sh ================================================ #!/usr/bin/env bash set -euo pipefail # Minimal wrapper to create a DMG using dmgbuild. # Usage: ./create_dmg.sh APP_PATH="${1:?App path required}" DMG_OUTPUT="${2:?DMG output path required}" VOLUME_NAME="${3:?Volume name required}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SETTINGS="$SCRIPT_DIR/dmgbuild_settings.py" BACKGROUND_DIR="$SCRIPT_DIR/.background" die() { echo "Error: $*" >&2 exit 1 } abs_path() { python3 -c 'import os,sys; print(os.path.abspath(sys.argv[1]))' "$1" } ensure_dmgbuild_and_badge_support() { if command -v dmgbuild >/dev/null 2>&1; then return 0 fi if ! command -v pip3 >/dev/null 2>&1; then die "dmgbuild is not installed and pip3 is not available. Please install dmgbuild." fi echo "dmgbuild not found — installing via pip3 (user scope)..." python3 -m pip install --user "dmgbuild[badge_icons]" || python3 -m pip install "dmgbuild[badge_icons]" USER_BIN="$(python3 -c 'import site,sys; print(site.getuserbase() + "/bin")')" export PATH="$USER_BIN:$PATH" } find_app_icns() { local app="$1" local info_plist="$app/Contents/Info.plist" if [ ! -f "$info_plist" ]; then return 1 fi local icon_file="" icon_file="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIconFile' "$info_plist" 2>/dev/null || true)" if [ -z "$icon_file" ]; then icon_file="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIconName' "$info_plist" 2>/dev/null || true)" fi if [ -n "$icon_file" ]; then if [[ "$icon_file" != *.icns ]]; then icon_file="$icon_file.icns" fi if [ -f "$app/Contents/Resources/$icon_file" ]; then echo "$app/Contents/Resources/$icon_file" return 0 fi fi # Fallback: any .icns inside the app bundle local candidate candidate="$(find "$app/Contents/Resources" -maxdepth 1 -name '*.icns' -print -quit 2>/dev/null || true)" if [ -n "$candidate" ] && [ -f "$candidate" ]; then echo "$candidate" return 0 fi return 1 } if [ ! -f "$SETTINGS" ]; then die "dmgbuild settings not found: $SETTINGS" fi ensure_dmgbuild_and_badge_support export DMG_APP_PATH="$(abs_path "$APP_PATH")" export DMG_VOLUME_NAME="$VOLUME_NAME" BACKGROUND_TIFF="$BACKGROUND_DIR/background.tiff" export DMG_BACKGROUND="$(abs_path "$BACKGROUND_TIFF")" # Badge icon: use the app's icon for badging the volume icon if DMG_ICON_ICNS="$(find_app_icns "$DMG_APP_PATH" 2>/dev/null)"; then export DMG_BADGE_ICON="$(abs_path "$DMG_ICON_ICNS")" echo "Using badge icon for DMG volume." else echo "No app icon found, skipping badge." fi echo "Creating DMG via dmgbuild: app=$DMG_APP_PATH output=$DMG_OUTPUT volume=$DMG_VOLUME_NAME" # Validate inputs early to give clearer errors for common typos if [ ! -e "$DMG_APP_PATH" ]; then echo "Error: App path not found: $DMG_APP_PATH" >&2 echo "Make sure you passed the correct .app path (e.g. Release/boringNotch.app)" >&2 exit 2 fi if [ ! -d "$DMG_APP_PATH" ]; then echo "Error: App path exists but is not a directory: $DMG_APP_PATH" >&2 exit 3 fi dmgbuild -s "$SETTINGS" "$DMG_VOLUME_NAME" "$DMG_OUTPUT" exit $? ================================================ FILE: Configuration/dmg/dmgbuild_settings.py ================================================ import os # dmgbuild settings file. This is read by the `dmgbuild` CLI (or Python API). # It uses environment variables exported by the shell wrapper script: # - DMG_APP_PATH: path to the .app bundle to put in the DMG # - DMG_VOLUME_NAME: volume name to display when the DMG is mounted # - DMG_BACKGROUND: absolute path to the background image to use APP_PATH = os.environ.get('DMG_APP_PATH') VOLUME_NAME = os.environ.get('DMG_VOLUME_NAME', 'boringNotch') BACKGROUND = os.environ.get('DMG_BACKGROUND', '') BADGE_ICON = os.environ.get('DMG_BADGE_ICON', '') # If DMG_BACKGROUND not provided, default to the hiDPI TIFF in .background. if not BACKGROUND: base = os.path.join(os.path.dirname(__file__), '.background', 'background.tiff') # Basic DMG metadata volume_name = VOLUME_NAME format = 'UDZO' compression_level = 9 # Files and symlinks to include in the DMG files = [APP_PATH] if APP_PATH else [] symlinks = {'Applications': '/Applications'} # Background image path (dmgbuild will copy this file into the DMG's .background) background = BACKGROUND # Window rectangle: ((left, top), (right, bottom)) window_rect = ((0, 0), (660, 400)) # Icon size (points) icon_size = 128 # Icon locations: map filename (or bundle name) -> (x, y) in window coords app_basename = os.path.basename(APP_PATH) if APP_PATH else 'boringNotch.app' icon_locations = { app_basename: (150, 180), 'Applications': (510, 180), } # Misc Finder options show_statusbar = False show_tabview = False show_toolbar = False # Optionally set a custom icon for the DMG volume (leave empty to skip) if BADGE_ICON and os.path.exists(BADGE_ICON): badge_icon = BADGE_ICON ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.md ================================================


Boring Notch
Boring Notch

TheBoringNotch Build & Test Discord Badge Ko-Fi

Say hello to **Boring Notch**, the coolest way to make your MacBook’s notch the star of the show! Say goodbye to boring status bars: with Boring Notch, your notch transforms into a dynamic music control center, complete with a vibrant visualizer and all the essential music controls you need. But that’s just the start! Boring Notch also offers calendar integration, a handy file shelf with AirDrop support, a complete MacOS HUD replacement and more!

Demo GIF

--- ## Installation **System Requirements:** - macOS **14 Sonoma** or later - Apple Silicon or Intel Mac --- ### Option 1: Download and Install Manually Download for macOS Once downloaded, open the `.dmg` and move **Boring Notch** to your `/Applications` folder. > [!IMPORTANT] > We don't have an Apple Developer account (yet 👀), so macOS will warn you that Boring Notch is from an unidentified developer on first launch. This is expected behavior. > > You'll need to bypass this before the app will open. You only need to do this once. Use one of the methods below. --- #### Recommended: Terminal (Always Works) This is the fastest and simplest method. It requires just one command and works reliably for all users, unlike System Settings, which occasionally doesn't. After moving Boring Notch to your Applications folder, run: ```bash xattr -dr com.apple.quarantine /Applications/boringNotch.app ``` Then open the app normally. --- #### Alternative: System Settings > [!NOTE] > This method doesn't work for all users. If this doesn't work, use the Terminal method above. 1. Try to open the app — you'll see a security warning. 2. Click **OK** to dismiss it. 3. Open **System Settings** > **Privacy & Security**. 4. Scroll to the bottom and click **Open Anyway** next to the Boring Notch warning. 5. Confirm if prompted. --- ### Option 2: Install via Homebrew You can also install using [Homebrew](https://brew.sh). The Homebrew installation automatically bypasses the macOS security warning described above. ```bash brew install --cask TheBoredTeam/boring-notch/boring-notch ``` ## Usage - Launch the app, and voilà—your notch is now the coolest part of your screen. - Hover over the notch to see it expand and reveal all its secrets. - Use the controls to manage your music like a rockstar. - Click the star in your menu bar to customize your notch to your heart's content. ## 📋 Roadmap - [x] Playback live activity 🎧 - [x] Calendar integration 📆 - [x] Reminders integration ☑️ - [x] Mirror 📷 - [x] Charging indicator and current percentage 🔋 - [x] Customizable gesture control 👆🏻 - [x] Shelf functionality with AirDrop 📚 - [x] Notch sizing customization, finetuning on different display sizes 🖥️ - [x] System HUD replacements (volume, brightness, backlight) 🎚️💡⌨️ - [ ] Bluetooth Live Activity (connect/disconnect for bluetooth devices) - [ ] Weather integration ⛅️ - [ ] Customizable Layout options 🛠️ - [ ] Lock Screen Widgets 🔒 - [ ] Extension system 🧩 - [ ] Notifications (under consideration) 🔔 ## Building from Source ### Prerequisites - **macOS 14 or later**: If you’re not on the latest macOS, we might need to send a search party. - **Xcode 16 or later**: This is where the magic happens, so make sure it’s up-to-date. ### Installation 1. **Clone the Repository**: ```bash git clone https://github.com/TheBoredTeam/boring.notch.git cd boring.notch ``` 2. **Open the Project in Xcode**: ```bash open boringNotch.xcodeproj ``` 3. **Build and Run**: - Click the "Run" button or press `Cmd + R`. Watch the magic unfold! ## 🤝 Contributing We’re all about good vibes and awesome contributions! Read [CONTRIBUTING.md](CONTRIBUTING.md) to learn how you can join the fun! ## Join our Discord Server Join The Boring Server! ## Star History Star History Chart ## Support us on Ko-fi! Support us on Ko-fi ## 🎉 Acknowledgments We would like to express our gratitude to the authors and maintainers of the open-source projects that made this possible. ## Notable Projects - **[MediaRemoteAdapter](https://github.com/ungive/mediaremote-adapter)** – An open-source project that allowed us to use the Now Playing source in macOS 15.4+ - **[NotchDrop](https://github.com/Lakr233/NotchDrop)** – An open-source project that has been instrumental in developing the first version of the "Shelf" feature in Boring Notch. For a full list of licenses and attributions, please see the [Third-Party Licenses](./THIRD_PARTY_LICENSES.md) file. ### Icon credits: [@maxtron95](https://github.com/maxtron95) ### Website credits: [@himanshhhhuv](https://github.com/himanshhhhuv) - **SwiftUI**: For making us look like coding wizards. - **You**: For being awesome and checking out **boring.notch**! ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Reporting a Vulnerability The Bored Team and community take security bugs in Boring Notch seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions. To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/TheBoredTeam/boring.notch/security/advisories/new) tab. The Bored Team will send a response indicating the next steps in handling your report. After the initial reply to your report, we will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance. Report security bugs in third-party dependencies to the person or team maintaining the package or dependency. ================================================ FILE: THIRD_PARTY_LICENSES ================================================ ----------------------------------------------------------------------------- BSD 3-Clause License applies to: - MediaRemoteAdapter, Copyright (c) 2025, Jonas van den Berg and contributors ----------------------------------------------------------------------------- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ----------------------------------------------------------------------------- The MIT License (MIT) applies to: - Calendr, Copyright (c) 2021 Carlos César Neves Enumo - DynamicNotchKit, Copyright (c) 2025 Kai Azim - NotchDrop Copyright (c) 2024 Lakr Aream ----------------------------------------------------------------------------- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- Mozilla Public License Version 2.0 applies to: - Parrot, Copyright (c) 2018, Aditya Vaidyam and Contributors ----------------------------------------------------------------------------- 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. ================================================ FILE: boringNotch/Assets.xcassets/AccentColor.colorset/Contents.json ================================================ { "colors" : [ { "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: boringNotch/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "filename" : "notch-stage-icon2 2.png", "idiom" : "mac", "scale" : "1x", "size" : "16x16" }, { "filename" : "notch-stage-icon2 5.png", "idiom" : "mac", "scale" : "2x", "size" : "16x16" }, { "filename" : "notch-stage-icon2 6.png", "idiom" : "mac", "scale" : "1x", "size" : "32x32" }, { "filename" : "notch-stage-icon2 11.png", "idiom" : "mac", "scale" : "2x", "size" : "32x32" }, { "filename" : "notch-stage-icon2 12.png", "idiom" : "mac", "scale" : "1x", "size" : "128x128" }, { "filename" : "notch-stage-icon2 13.png", "idiom" : "mac", "scale" : "2x", "size" : "128x128" }, { "filename" : "notch-stage-icon2 7.png", "idiom" : "mac", "scale" : "1x", "size" : "256x256" }, { "filename" : "notch-stage-icon2 8.png", "idiom" : "mac", "scale" : "2x", "size" : "256x256" }, { "filename" : "notch-stage-icon2 9.png", "idiom" : "mac", "scale" : "1x", "size" : "512x512" }, { "filename" : "notch-stage-icon2 10.png", "idiom" : "mac", "scale" : "2x", "size" : "512x512" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: boringNotch/Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: boringNotch/Assets.xcassets/Github.imageset/Contents.json ================================================ { "images" : [ { "filename" : "GitHub Mark White.svg", "idiom" : "universal", "scale" : "1x" }, { "filename" : "GitHub Mark White 1.svg", "idiom" : "universal", "scale" : "2x" }, { "filename" : "GitHub Mark White 2.svg", "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: boringNotch/Assets.xcassets/bolt.imageset/Contents.json ================================================ { "images" : [ { "filename" : "bolt.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: boringNotch/Assets.xcassets/chrome.imageset/Contents.json ================================================ { "images" : [ { "filename" : "Google Chrome macOS BigSur Icon.png", "idiom" : "universal", "scale" : "1x" }, { "filename" : "Google Chrome macOS BigSur Icon 1.png", "idiom" : "universal", "scale" : "2x" }, { "filename" : "Google Chrome macOS BigSur Icon 2.png", "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: boringNotch/Assets.xcassets/defaultmusic.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "scale" : "1x" }, { "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: boringNotch/Assets.xcassets/logo.imageset/Contents.json ================================================ { "images" : [ { "filename" : "256-mac 1.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: boringNotch/Assets.xcassets/logo2.imageset/Contents.json ================================================ { "images" : [ { "filename" : "BoringNotch icon.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: boringNotch/Assets.xcassets/plug.imageset/Contents.json ================================================ { "images" : [ { "filename" : "plug.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: boringNotch/Assets.xcassets/sparkle.imageset/Contents.json ================================================ { "images" : [ { "filename" : "sparkle.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: boringNotch/Assets.xcassets/spotlight.imageset/Contents.json ================================================ { "images" : [ { "filename" : "spotlight.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: boringNotch/Assets.xcassets/theboringteam.imageset/Contents.json ================================================ { "images" : [ { "filename" : "TheBoringTeam.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: boringNotch/BoringViewCoordinator.swift ================================================ // // BoringViewCoordinator.swift // boringNotch // // Created by Alexander on 2024-11-20. // import AppKit import Combine import Defaults import SwiftUI enum SneakContentType { case brightness case volume case backlight case music case mic case battery case download } struct sneakPeek { var show: Bool = false var type: SneakContentType = .music var value: CGFloat = 0 var icon: String = "" } struct SharedSneakPeek: Codable { var show: Bool var type: String var value: String var icon: String } enum BrowserType { case chromium case safari } struct ExpandedItem { var show: Bool = false var type: SneakContentType = .battery var value: CGFloat = 0 var browser: BrowserType = .chromium } @MainActor class BoringViewCoordinator: ObservableObject { static let shared = BoringViewCoordinator() @Published var currentView: NotchViews = .home @Published var helloAnimationRunning: Bool = false private var sneakPeekDispatch: DispatchWorkItem? private var expandingViewDispatch: DispatchWorkItem? private var hudEnableTask: Task? @AppStorage("firstLaunch") var firstLaunch: Bool = true @AppStorage("showWhatsNew") var showWhatsNew: Bool = true @AppStorage("musicLiveActivityEnabled") var musicLiveActivityEnabled: Bool = true @AppStorage("currentMicStatus") var currentMicStatus: Bool = true @AppStorage("alwaysShowTabs") var alwaysShowTabs: Bool = true { didSet { if !alwaysShowTabs { openLastTabByDefault = false if ShelfStateViewModel.shared.isEmpty || !Defaults[.openShelfByDefault] { currentView = .home } } } } @AppStorage("openLastTabByDefault") var openLastTabByDefault: Bool = false { didSet { if openLastTabByDefault { alwaysShowTabs = true } } } @Default(.hudReplacement) var hudReplacement: Bool // Legacy storage for migration @AppStorage("preferred_screen_name") private var legacyPreferredScreenName: String? // New UUID-based storage @AppStorage("preferred_screen_uuid") var preferredScreenUUID: String? { didSet { if let uuid = preferredScreenUUID { selectedScreenUUID = uuid } NotificationCenter.default.post(name: Notification.Name.selectedScreenChanged, object: nil) } } @Published var selectedScreenUUID: String = NSScreen.main?.displayUUID ?? "" @Published var optionKeyPressed: Bool = true private var accessibilityObserver: Any? private var hudReplacementCancellable: AnyCancellable? private init() { // Perform migration from name-based to UUID-based storage if preferredScreenUUID == nil, let legacyName = legacyPreferredScreenName { // Try to find screen by name and migrate to UUID if let screen = NSScreen.screens.first(where: { $0.localizedName == legacyName }), let uuid = screen.displayUUID { preferredScreenUUID = uuid NSLog("✅ Migrated display preference from name '\(legacyName)' to UUID '\(uuid)'") } else { // Fallback to main screen if legacy screen not found preferredScreenUUID = NSScreen.main?.displayUUID NSLog("⚠️ Could not find display named '\(legacyName)', falling back to main screen") } // Clear legacy value after migration legacyPreferredScreenName = nil } else if preferredScreenUUID == nil { // No legacy value, use main screen preferredScreenUUID = NSScreen.main?.displayUUID } selectedScreenUUID = preferredScreenUUID ?? NSScreen.main?.displayUUID ?? "" // Observe changes to accessibility authorization and react accordingly accessibilityObserver = NotificationCenter.default.addObserver( forName: Notification.Name.accessibilityAuthorizationChanged, object: nil, queue: .main ) { _ in Task { @MainActor in if Defaults[.hudReplacement] { await MediaKeyInterceptor.shared.start(promptIfNeeded: false) } } } // Observe changes to hudReplacement hudReplacementCancellable = Defaults.publisher(.hudReplacement) .sink { [weak self] change in Task { @MainActor in guard let self = self else { return } self.hudEnableTask?.cancel() self.hudEnableTask = nil if change.newValue { self.hudEnableTask = Task { @MainActor in let granted = await XPCHelperClient.shared.ensureAccessibilityAuthorization(promptIfNeeded: true) if Task.isCancelled { return } if granted { await MediaKeyInterceptor.shared.start() } else { Defaults[.hudReplacement] = false } } } else { MediaKeyInterceptor.shared.stop() } } } Task { @MainActor in helloAnimationRunning = firstLaunch if Defaults[.hudReplacement] { let authorized = await XPCHelperClient.shared.isAccessibilityAuthorized() if !authorized { Defaults[.hudReplacement] = false } else { await MediaKeyInterceptor.shared.start(promptIfNeeded: false) } } } } @objc func sneakPeekEvent(_ notification: Notification) { let decoder = JSONDecoder() if let decodedData = try? decoder.decode( SharedSneakPeek.self, from: notification.userInfo?.first?.value as! Data) { let contentType = decodedData.type == "brightness" ? SneakContentType.brightness : decodedData.type == "volume" ? SneakContentType.volume : decodedData.type == "backlight" ? SneakContentType.backlight : decodedData.type == "mic" ? SneakContentType.mic : SneakContentType.brightness let formatter = NumberFormatter() formatter.locale = Locale(identifier: "en_US_POSIX") formatter.numberStyle = .decimal let value = CGFloat((formatter.number(from: decodedData.value) ?? 0.0).floatValue) let icon = decodedData.icon print("Decoded: \(decodedData), Parsed value: \(value)") toggleSneakPeek(status: decodedData.show, type: contentType, value: value, icon: icon) } else { print("Failed to decode JSON data") } } func toggleSneakPeek( status: Bool, type: SneakContentType, duration: TimeInterval = 1.5, value: CGFloat = 0, icon: String = "" ) { sneakPeekDuration = duration if type != .music { // close() if !Defaults[.hudReplacement] { return } } Task { @MainActor in withAnimation(.smooth) { self.sneakPeek.show = status self.sneakPeek.type = type self.sneakPeek.value = value self.sneakPeek.icon = icon } } if type == .mic { currentMicStatus = value == 1 } } private var sneakPeekDuration: TimeInterval = 1.5 private var sneakPeekTask: Task? // Helper function to manage sneakPeek timer using Swift Concurrency private func scheduleSneakPeekHide(after duration: TimeInterval) { sneakPeekTask?.cancel() sneakPeekTask = Task { [weak self] in try? await Task.sleep(for: .seconds(duration)) guard let self = self, !Task.isCancelled else { return } await MainActor.run { withAnimation { self.toggleSneakPeek(status: false, type: .music) self.sneakPeekDuration = 1.5 } } } } @Published var sneakPeek: sneakPeek = .init() { didSet { if sneakPeek.show { scheduleSneakPeekHide(after: sneakPeekDuration) } else { sneakPeekTask?.cancel() } } } func toggleExpandingView( status: Bool, type: SneakContentType, value: CGFloat = 0, browser: BrowserType = .chromium ) { Task { @MainActor in withAnimation(.smooth) { self.expandingView.show = status self.expandingView.type = type self.expandingView.value = value self.expandingView.browser = browser } } } private var expandingViewTask: Task? @Published var expandingView: ExpandedItem = .init() { didSet { if expandingView.show { expandingViewTask?.cancel() let duration: TimeInterval = (expandingView.type == .download ? 2 : 3) let currentType = expandingView.type expandingViewTask = Task { [weak self] in try? await Task.sleep(for: .seconds(duration)) guard let self = self, !Task.isCancelled else { return } self.toggleExpandingView(status: false, type: currentType) } } else { expandingViewTask?.cancel() } } } func showEmpty() { currentView = .home } } ================================================ FILE: boringNotch/ContentView.swift ================================================ // // ContentView.swift // boringNotchApp // // Created by Harsh Vardhan Goswami on 02/08/24 // Modified by Richard Kunkli on 24/08/2024. // import AVFoundation import Combine import Defaults import KeyboardShortcuts import SwiftUI import SwiftUIIntrospect @MainActor struct ContentView: View { @EnvironmentObject var vm: BoringViewModel @ObservedObject var webcamManager = WebcamManager.shared @ObservedObject var coordinator = BoringViewCoordinator.shared @ObservedObject var musicManager = MusicManager.shared @ObservedObject var batteryModel = BatteryStatusViewModel.shared @ObservedObject var brightnessManager = BrightnessManager.shared @ObservedObject var volumeManager = VolumeManager.shared @State private var hoverTask: Task? @State private var isHovering: Bool = false @State private var anyDropDebounceTask: Task? @State private var gestureProgress: CGFloat = .zero @State private var haptics: Bool = false @Namespace var albumArtNamespace @Default(.useMusicVisualizer) var useMusicVisualizer @Default(.showNotHumanFace) var showNotHumanFace // Shared interactive spring for movement/resizing to avoid conflicting animations private let animationSpring = Animation.interactiveSpring(response: 0.38, dampingFraction: 0.8, blendDuration: 0) private let extendedHoverPadding: CGFloat = 30 private let zeroHeightHoverPadding: CGFloat = 10 private var topCornerRadius: CGFloat { ((vm.notchState == .open) && Defaults[.cornerRadiusScaling]) ? cornerRadiusInsets.opened.top : cornerRadiusInsets.closed.top } private var currentNotchShape: NotchShape { NotchShape( topCornerRadius: topCornerRadius, bottomCornerRadius: ((vm.notchState == .open) && Defaults[.cornerRadiusScaling]) ? cornerRadiusInsets.opened.bottom : cornerRadiusInsets.closed.bottom ) } private var computedChinWidth: CGFloat { var chinWidth: CGFloat = vm.closedNotchSize.width if coordinator.expandingView.type == .battery && coordinator.expandingView.show && vm.notchState == .closed && Defaults[.showPowerStatusNotifications] { chinWidth = 640 } else if (!coordinator.expandingView.show || coordinator.expandingView.type == .music) && vm.notchState == .closed && (musicManager.isPlaying || !musicManager.isPlayerIdle) && coordinator.musicLiveActivityEnabled && !vm.hideOnClosed { chinWidth += (2 * max(0, vm.effectiveClosedNotchHeight - 12) + 20) } else if !coordinator.expandingView.show && vm.notchState == .closed && (!musicManager.isPlaying && musicManager.isPlayerIdle) && Defaults[.showNotHumanFace] && !vm.hideOnClosed { chinWidth += (2 * max(0, vm.effectiveClosedNotchHeight - 12) + 20) } return chinWidth } var body: some View { // Calculate scale based on gesture progress only let gestureScale: CGFloat = { guard gestureProgress != 0 else { return 1.0 } let scaleFactor = 1.0 + gestureProgress * 0.01 return max(0.6, scaleFactor) }() ZStack(alignment: .top) { VStack(spacing: 0) { let mainLayout = NotchLayout() .frame(alignment: .top) .padding( .horizontal, vm.notchState == .open ? Defaults[.cornerRadiusScaling] ? (cornerRadiusInsets.opened.top) : (cornerRadiusInsets.opened.bottom) : cornerRadiusInsets.closed.bottom ) .padding([.horizontal, .bottom], vm.notchState == .open ? 12 : 0) .background(.black) .clipShape(currentNotchShape) .overlay(alignment: .top) { Rectangle() .fill(.black) .frame(height: 1) .padding(.horizontal, topCornerRadius) } .shadow( color: ((vm.notchState == .open || isHovering) && Defaults[.enableShadow]) ? .black.opacity(0.7) : .clear, radius: Defaults[.cornerRadiusScaling] ? 6 : 4 ) .padding( .bottom, vm.effectiveClosedNotchHeight == 0 ? 10 : 0 ) mainLayout .frame(height: vm.notchState == .open ? vm.notchSize.height : nil) .conditionalModifier(true) { view in let openAnimation = Animation.spring(response: 0.42, dampingFraction: 0.8, blendDuration: 0) let closeAnimation = Animation.spring(response: 0.45, dampingFraction: 1.0, blendDuration: 0) return view .animation(vm.notchState == .open ? openAnimation : closeAnimation, value: vm.notchState) .animation(.smooth, value: gestureProgress) } .contentShape(Rectangle()) .onHover { hovering in handleHover(hovering) } .onTapGesture { doOpen() } .conditionalModifier(Defaults[.enableGestures]) { view in view .panGesture(direction: .down) { translation, phase in handleDownGesture(translation: translation, phase: phase) } } .conditionalModifier(Defaults[.closeGestureEnabled] && Defaults[.enableGestures]) { view in view .panGesture(direction: .up) { translation, phase in handleUpGesture(translation: translation, phase: phase) } } .onReceive(NotificationCenter.default.publisher(for: .sharingDidFinish)) { _ in if vm.notchState == .open && !isHovering && !vm.isBatteryPopoverActive { hoverTask?.cancel() hoverTask = Task { try? await Task.sleep(for: .milliseconds(100)) guard !Task.isCancelled else { return } await MainActor.run { if self.vm.notchState == .open && !self.isHovering && !self.vm.isBatteryPopoverActive && !SharingStateManager.shared.preventNotchClose { self.vm.close() } } } } } .onChange(of: vm.notchState) { _, newState in if newState == .closed && isHovering { withAnimation { isHovering = false } } } .onChange(of: vm.isBatteryPopoverActive) { if !vm.isBatteryPopoverActive && !isHovering && vm.notchState == .open && !SharingStateManager.shared.preventNotchClose { hoverTask?.cancel() hoverTask = Task { try? await Task.sleep(for: .milliseconds(100)) guard !Task.isCancelled else { return } await MainActor.run { if !self.vm.isBatteryPopoverActive && !self.isHovering && self.vm.notchState == .open && !SharingStateManager.shared.preventNotchClose { self.vm.close() } } } } } .sensoryFeedback(.alignment, trigger: haptics) .contextMenu { Button("Settings") { SettingsWindowController.shared.showWindow() } .keyboardShortcut(KeyEquivalent(","), modifiers: .command) // Button("Edit") { // Doesnt work.... // let dn = DynamicNotch(content: EditPanelView()) // dn.toggle() // } // .keyboardShortcut("E", modifiers: .command) } if vm.chinHeight > 0 { Rectangle() .fill(Color.black.opacity(0.01)) .frame(width: computedChinWidth, height: vm.chinHeight) } } } .padding(.bottom, 8) .frame(maxWidth: windowSize.width, maxHeight: windowSize.height, alignment: .top) .compositingGroup() .scaleEffect( x: gestureScale, y: gestureScale, anchor: .top ) .animation(.smooth, value: gestureProgress) .background(dragDetector) .preferredColorScheme(.dark) .environmentObject(vm) .onChange(of: vm.anyDropZoneTargeting) { _, isTargeted in anyDropDebounceTask?.cancel() if isTargeted { if vm.notchState == .closed { coordinator.currentView = .shelf doOpen() } return } anyDropDebounceTask = Task { @MainActor in try? await Task.sleep(for: .milliseconds(500)) guard !Task.isCancelled else { return } if vm.dropEvent { vm.dropEvent = false return } vm.dropEvent = false if !SharingStateManager.shared.preventNotchClose { vm.close() } } } } @ViewBuilder func NotchLayout() -> some View { VStack(alignment: .leading) { VStack(alignment: .leading) { if coordinator.helloAnimationRunning { Spacer() HelloAnimation(onFinish: { vm.closeHello() }).frame( width: getClosedNotchSize().width, height: 80 ) .padding(.top, 40) Spacer() } else { if coordinator.expandingView.type == .battery && coordinator.expandingView.show && vm.notchState == .closed && Defaults[.showPowerStatusNotifications] { HStack(spacing: 0) { HStack { Text(batteryModel.statusText) .font(.subheadline) .foregroundStyle(.white) } Rectangle() .fill(.black) .frame(width: vm.closedNotchSize.width + 10) HStack { BoringBatteryView( batteryWidth: 30, isCharging: batteryModel.isCharging, isInLowPowerMode: batteryModel.isInLowPowerMode, isPluggedIn: batteryModel.isPluggedIn, levelBattery: batteryModel.levelBattery, isForNotification: true ) } .frame(width: 76, alignment: .trailing) } .frame(height: vm.effectiveClosedNotchHeight, alignment: .center) } else if coordinator.sneakPeek.show && Defaults[.inlineHUD] && (coordinator.sneakPeek.type != .music) && (coordinator.sneakPeek.type != .battery) && vm.notchState == .closed { InlineHUD(type: $coordinator.sneakPeek.type, value: $coordinator.sneakPeek.value, icon: $coordinator.sneakPeek.icon, hoverAnimation: $isHovering, gestureProgress: $gestureProgress) .transition(.opacity) } else if (!coordinator.expandingView.show || coordinator.expandingView.type == .music) && vm.notchState == .closed && (musicManager.isPlaying || !musicManager.isPlayerIdle) && coordinator.musicLiveActivityEnabled && !vm.hideOnClosed { MusicLiveActivity() .frame(alignment: .center) } else if !coordinator.expandingView.show && vm.notchState == .closed && (!musicManager.isPlaying && musicManager.isPlayerIdle) && Defaults[.showNotHumanFace] && !vm.hideOnClosed { BoringFaceAnimation() } else if vm.notchState == .open { BoringHeader() .frame(height: max(24, vm.effectiveClosedNotchHeight)) .opacity(gestureProgress != 0 ? 1.0 - min(abs(gestureProgress) * 0.1, 0.3) : 1.0) } else { Rectangle().fill(.clear).frame(width: vm.closedNotchSize.width - 20, height: vm.effectiveClosedNotchHeight) } if coordinator.sneakPeek.show { if (coordinator.sneakPeek.type != .music) && (coordinator.sneakPeek.type != .battery) && !Defaults[.inlineHUD] && vm.notchState == .closed { SystemEventIndicatorModifier( eventType: $coordinator.sneakPeek.type, value: $coordinator.sneakPeek.value, icon: $coordinator.sneakPeek.icon, sendEventBack: { newVal in switch coordinator.sneakPeek.type { case .volume: VolumeManager.shared.setAbsolute(Float32(newVal)) case .brightness: BrightnessManager.shared.setAbsolute(value: Float32(newVal)) default: break } } ) .padding(.bottom, 10) .padding(.leading, 4) .padding(.trailing, 8) } // Old sneak peek music else if coordinator.sneakPeek.type == .music { if vm.notchState == .closed && !vm.hideOnClosed && Defaults[.sneakPeekStyles] == .standard { HStack(alignment: .center) { Image(systemName: "music.note") GeometryReader { geo in MarqueeText(.constant(musicManager.songTitle + " - " + musicManager.artistName), textColor: Defaults[.playerColorTinting] ? Color(nsColor: musicManager.avgColor).ensureMinimumBrightness(factor: 0.6) : .gray, minDuration: 1, frameWidth: geo.size.width) } } .foregroundStyle(.gray) .padding(.bottom, 10) } } } } } .conditionalModifier((coordinator.sneakPeek.show && (coordinator.sneakPeek.type == .music) && vm.notchState == .closed && !vm.hideOnClosed && Defaults[.sneakPeekStyles] == .standard) || (coordinator.sneakPeek.show && (coordinator.sneakPeek.type != .music) && (vm.notchState == .closed))) { view in view .fixedSize() } .zIndex(2) if vm.notchState == .open { VStack { switch coordinator.currentView { case .home: NotchHomeView(albumArtNamespace: albumArtNamespace) case .shelf: ShelfView() } } .transition( .scale(scale: 0.8, anchor: .top) .combined(with: .opacity) .animation(.smooth(duration: 0.35)) ) .zIndex(1) .allowsHitTesting(vm.notchState == .open) .opacity(gestureProgress != 0 ? 1.0 - min(abs(gestureProgress) * 0.1, 0.3) : 1.0) } } .onDrop(of: [.fileURL, .url, .utf8PlainText, .plainText, .data], delegate: GeneralDropTargetDelegate(isTargeted: $vm.generalDropTargeting)) } @ViewBuilder func BoringFaceAnimation() -> some View { HStack { HStack { Rectangle() .fill(.clear) .frame( width: max(0, vm.effectiveClosedNotchHeight - 12), height: max(0, vm.effectiveClosedNotchHeight - 12) ) Rectangle() .fill(.black) .frame(width: vm.closedNotchSize.width - 20) MinimalFaceFeatures() } }.frame( height: vm.effectiveClosedNotchHeight, alignment: .center ) } @ViewBuilder func MusicLiveActivity() -> some View { HStack { Image(nsImage: musicManager.albumArt) .resizable() .clipped() .clipShape( RoundedRectangle( cornerRadius: MusicPlayerImageSizes.cornerRadiusInset.closed) ) .matchedGeometryEffect(id: "albumArt", in: albumArtNamespace) .frame( width: max(0, vm.effectiveClosedNotchHeight - 12), height: max(0, vm.effectiveClosedNotchHeight - 12) ) Rectangle() .fill(.black) .overlay( HStack(alignment: .top) { if coordinator.expandingView.show && coordinator.expandingView.type == .music { MarqueeText( .constant(musicManager.songTitle), textColor: Defaults[.coloredSpectrogram] ? Color(nsColor: musicManager.avgColor) : Color.gray, minDuration: 0.4, frameWidth: 100 ) .opacity( (coordinator.expandingView.show && Defaults[.sneakPeekStyles] == .inline) ? 1 : 0 ) Spacer(minLength: vm.closedNotchSize.width) // Song Artist Text(musicManager.artistName) .lineLimit(1) .truncationMode(.tail) .foregroundStyle( Defaults[.coloredSpectrogram] ? Color(nsColor: musicManager.avgColor) : Color.gray ) .opacity( (coordinator.expandingView.show && coordinator.expandingView.type == .music && Defaults[.sneakPeekStyles] == .inline) ? 1 : 0 ) } } ) .frame( width: (coordinator.expandingView.show && coordinator.expandingView.type == .music && Defaults[.sneakPeekStyles] == .inline) ? 380 : vm.closedNotchSize.width + -cornerRadiusInsets.closed.top ) HStack { if useMusicVisualizer { Rectangle() .fill( Defaults[.coloredSpectrogram] ? Color(nsColor: musicManager.avgColor).gradient : Color.gray.gradient ) .frame(width: 50, alignment: .center) .matchedGeometryEffect(id: "spectrum", in: albumArtNamespace) .mask { AudioSpectrumView(isPlaying: $musicManager.isPlaying) .frame(width: 16, height: 12) } } else { LottieAnimationContainer() .frame(maxWidth: .infinity, maxHeight: .infinity) } } .frame( width: max( 0, vm.effectiveClosedNotchHeight - 12 + gestureProgress / 2 ), height: max( 0, vm.effectiveClosedNotchHeight - 12 ), alignment: .center ) } .frame( height: vm.effectiveClosedNotchHeight, alignment: .center ) } @ViewBuilder var dragDetector: some View { if Defaults[.boringShelf] && vm.notchState == .closed { Color.clear .frame(maxWidth: .infinity, maxHeight: .infinity) .contentShape(Rectangle()) .onDrop(of: [.fileURL, .url, .utf8PlainText, .plainText, .data], isTargeted: $vm.dragDetectorTargeting) { providers in vm.dropEvent = true ShelfStateViewModel.shared.load(providers) return true } } else { EmptyView() } } private func doOpen() { withAnimation(animationSpring) { vm.open() } } // MARK: - Hover Management private func handleHover(_ hovering: Bool) { if coordinator.firstLaunch { return } hoverTask?.cancel() if hovering { withAnimation(animationSpring) { isHovering = true } if vm.notchState == .closed && Defaults[.enableHaptics] { haptics.toggle() } guard vm.notchState == .closed, !coordinator.sneakPeek.show, Defaults[.openNotchOnHover] else { return } hoverTask = Task { try? await Task.sleep(for: .seconds(Defaults[.minimumHoverDuration])) guard !Task.isCancelled else { return } await MainActor.run { guard self.vm.notchState == .closed, self.isHovering, !self.coordinator.sneakPeek.show else { return } self.doOpen() } } } else { hoverTask = Task { try? await Task.sleep(for: .milliseconds(100)) guard !Task.isCancelled else { return } await MainActor.run { withAnimation(animationSpring) { self.isHovering = false } if self.vm.notchState == .open && !self.vm.isBatteryPopoverActive && !SharingStateManager.shared.preventNotchClose { self.vm.close() } } } } } // MARK: - Gesture Handling private func handleDownGesture(translation: CGFloat, phase: NSEvent.Phase) { guard vm.notchState == .closed else { return } if phase == .ended { withAnimation(animationSpring) { gestureProgress = .zero } return } withAnimation(animationSpring) { gestureProgress = (translation / Defaults[.gestureSensitivity]) * 20 } if translation > Defaults[.gestureSensitivity] { if Defaults[.enableHaptics] { haptics.toggle() } withAnimation(animationSpring) { gestureProgress = .zero } doOpen() } } private func handleUpGesture(translation: CGFloat, phase: NSEvent.Phase) { guard vm.notchState == .open && !vm.isHoveringCalendar else { return } withAnimation(animationSpring) { gestureProgress = (translation / Defaults[.gestureSensitivity]) * -20 } if phase == .ended { withAnimation(animationSpring) { gestureProgress = .zero } } if translation > Defaults[.gestureSensitivity] { withAnimation(animationSpring) { isHovering = false } if !SharingStateManager.shared.preventNotchClose { gestureProgress = .zero vm.close() } if Defaults[.enableHaptics] { haptics.toggle() } } } } struct FullScreenDropDelegate: DropDelegate { @Binding var isTargeted: Bool let onDrop: () -> Void func dropEntered(info _: DropInfo) { isTargeted = true } func dropExited(info _: DropInfo) { isTargeted = false } func performDrop(info _: DropInfo) -> Bool { isTargeted = false onDrop() return true } } struct GeneralDropTargetDelegate: DropDelegate { @Binding var isTargeted: Bool func dropEntered(info: DropInfo) { isTargeted = true } func dropExited(info: DropInfo) { isTargeted = false } func dropUpdated(info: DropInfo) -> DropProposal? { return DropProposal(operation: .cancel) } func performDrop(info: DropInfo) -> Bool { return false } } #Preview { let vm = BoringViewModel() vm.open() return ContentView() .environmentObject(vm) .frame(width: vm.notchSize.width, height: vm.notchSize.height) } ================================================ FILE: boringNotch/Info.plist ================================================ NSAppTransportSecurity NSAllowsArbitraryLoads SUEnableDownloaderService SUEnableInstallerLauncherService SUFeedURL https://TheBoredTeam.github.io/boring.notch/appcast.xml SUPublicEDKey B1Y47t8C/v8ImurYA+9arEsuCrpxwJSviekiflMElbI= UTImportedTypeDeclarations ================================================ FILE: boringNotch/Localizable.xcstrings ================================================ { "sourceLanguage" : "en", "strings" : { "" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "" } } } }, " – %lld" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : " – %lld" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : " – %lld" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : " – %lld" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : " – %lld" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : " – %lld" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : " – %lld" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : " – %lld" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : " – %lld" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : " – %lld" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : " – %lld" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : " – %lld" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : " – %lld" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : " – %lld" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : " – %lld" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : " – %lld" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : " – %lld" } } } }, "'Now Playing' was the only option on previous versions and works with all media apps." : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "'Now Playing' was the only option on previous versions and works with all media apps." } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "\"Nyní hraje\" byla jediná možnost na předchozích verzích a funguje se všemi mediálními aplikacemi." } }, "de" : { "stringUnit" : { "state" : "translated", "value" : "\"Jetzt läuft\" war die einzige Option auf vorherigen Versionen und funktioniert mit allen Medien-Apps." } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "'Now Playing' was the only option on previous versions and works with all media apps." } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "'Now Playing' was the only option on previous versions and works with all media apps." } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "\"Ahora suena\" era la única opción disponible en versiones anteriores y funciona con todas las aplicaciones multimedia." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "“Lecture en cours” était la seule option dans les versions précédentes et fonctionne avec toutes les applications multimédias." } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "'Now Playing' was the only option on previous versions and works with all media apps." } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "'In riproduzione' era l’unica opzione nelle versioni precedenti e funziona con tutte le app multimediali." } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "이전 버전에서는 ‘현재 재생 중' 만 선택할 수 있었으며, 모든 미디어 애플리케이션과 호환됩니다." } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "'Teraz odtwarzane' było jedyną opcją w poprzednich wersjach i działa ze wszystkimi multimediami." } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "'Tocando agora' era a única opção em versões anteriores e funciona em todos os aplicativos de mídia." } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "'Сейчас играть' была единственной опцией в предыдущих версиях и работает со всеми медиа-приложениями." } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "'Şimdi Çalınan' önceki sürümlerdeki tek seçenekti ve tüm medya uygulamalarıyla çalışır." } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "'Зараз грає' був єдиним варіантом на попередніх версіях і працює з усіма медіа застосунками." } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "“当前播放”是历史版本中的唯一选项。该选项可兼容所有媒体应用。" } } } }, "(%@)" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "(%@)" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "(%@)" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "(%@)" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "(%@)" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "(%@)" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "(%@)" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "(%@)" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "(%@)" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "(%@)" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "(%@)" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "(%@)" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "(%@)" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "(%@)" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "(%@)" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "(%@)" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "(%@)" } } } }, "%.0f seconds" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "%.0f seconds" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "%.0f sekund" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "%.0f Sekunden" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "%.0f seconds" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "%.0f seconds" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "%.0f segundos" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%.0f secondes" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "%.0f seconds" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "%.0f secondi" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "%.0f 초" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "%.0f sekundy" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "%.0f segundos" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "%.0f секунды" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "%.0f saniye" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "%.0f секунд" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "%.0f 秒" } } } }, "%.1fs" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "%.1fs" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "%.1fs" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "%.1fs" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "%.1fs" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "%.1fs" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "%.1fs" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "%.1fs" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "%.1fs" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "%.1fs" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "%.1fs" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "%.1fs" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "%.1fs" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "%.1fs" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "%.1fs" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "%.1fs" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "%.1fs" } } } }, "%@" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "%@" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "%@" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "%@" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "%@" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "%@" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "%@" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "%@" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "%@" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "%@" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "%@" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "%@" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "%@" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "%@" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "%@" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "%@" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "%@" } } } }, "%d%%" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "%d%%" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "%d%%" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "%d%%" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "%d%%" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "%d%%" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "%d%%" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "%d%%" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "%d%%" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "%d%%" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "%d%%" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "%d%%" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "%d%%" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "%d%%" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "%d%%" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "%d%%" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "%d%%" } } } }, "%lld" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "%lld" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "%lld" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "%lld" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "%lld" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "%lld" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "%lld" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "%lld" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "%lld" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "%lld" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "%lld" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "%lld" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "%lld" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "%lld" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "%lld" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "%lld" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "%lld" } } } }, "%lld%%" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "%lld%%" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "%lld%%" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "%lld%%" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "%lld%%" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "%lld%%" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "%lld%%" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "%lld%%" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "%lld%%" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "%lld%%" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "%lld%%" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "%lld%%" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "%lld%%" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "%lld%%" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "%lld%%" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "%lld%%" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "%lld%%" } } } }, "• Bug fixes" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "• Bug fixes" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "• Opravy chyb" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "• Fehlerbehebungen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "• Bug fixes" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "• Bug fixes" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "• Corrección de errores" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "• Corrections de bugs" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "• Bug fixes" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "• Correzione bug" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "수정된 버그들" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Poprawki błędów" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Correções de bugs" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "• Исправления ошибок" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "• Hata düzeltmeleri" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "• Виправлення помилок" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "• Bug 修复" } } } }, "• Improved performance" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "• Improved performance" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "• Zlepšený výkon" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "• Verbesserte Leistung" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "• Improved performance" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "• Improved performance" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "• Rendimiento mejorado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Amélioration des performances" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "• Improved performance" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "• Prestazioni migliorate" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "• 향상된 성능" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Poprawiona wydajność" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Desempenho melhorado" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "• повышение эффективности" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Geliştirilmiş performans" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "• Покращено продуктивність" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "• 提升性能" } } } }, "• New feature 1" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "• New feature 1" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "• Nová funkce 1" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "• Neue Funktion 1" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "• New feature 1" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "• New feature 1" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "• Nueva funcionalidad 1" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Nouvelle fonctionnalité 1" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "• New feature 1" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "• Nuova funzionalità 1" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "• 새 기능 1" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Nowa funkcja 1" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Novo recurso 1" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "• New feature 1" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Yeni özellik 1" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "• Нова функція 1" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "• 新功能1" } } } }, "About" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "About" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "O aplikaci" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Über" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "About" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "About" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Acerca de" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "À propos" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "About" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Info" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "정보" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "O programie" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Sobre" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "О программе" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Hakkında" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Про додаток" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "关于" } } } }, "Accent color" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Accent color" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Barva odstínu" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Akzentfarbe" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Accent color" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Accent colour" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Color de resaltado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Couleur d'accent" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Accent color" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Colore in rilievo" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "강조 색상" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Kolor akcentu" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Cor de destaque" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Основной цвет" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Vurgu rengi" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Колір відтінку" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "首选颜色" } } } }, "Access Denied" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Access Denied" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Přístup zamítnut" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Zugriff verweigert" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Access Denied" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Access Denied" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Acceso denegado" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Accès Refusé" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Access Denied" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Accesso Negato" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "권한 거부됨" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Odmowa dostępu" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Acesso Negado" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Доступ запрещен" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Erişim Reddedildi" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Доступ заборонено" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "访问被拒绝" } } } }, "Accessibility access is required to replace the system HUD." : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Accessibility access is required to replace the system HUD." } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Accessibility access is required to replace the system HUD." } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Der Zugriff auf Barrierefreiheit ist erforderlich, um das System HUD zu ersetzen." } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Accessibility access is required to replace the system HUD." } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Accessibility access is required to replace the system HUD." } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Accessibility access is required to replace the system HUD." } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Accessibility access is required to replace the system HUD." } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Accessibility access is required to replace the system HUD." } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Accessibility access is required to replace the system HUD." } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "시스템 HUD를 교체하기 위해서는 접근성 권한이 필요합니다." } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Zezwolenie dostępności jest wymagane, żeby zastąpić system HUD." } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Accessibility access is required to replace the system HUD." } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Accessibility access is required to replace the system HUD." } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Accessibility access is required to replace the system HUD." } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Accessibility access is required to replace the system HUD." } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Accessibility access is required to replace the system HUD." } } } }, "Add" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Add" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Přidat" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Hinzufügen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Add" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Add" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Añadir" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Ajouter" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Add" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Aggiungi" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "추가" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Dodaj" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Adicionar" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Добавить" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Ekle" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Додати" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "添加" } } } }, "Add manually" : { "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Add manually" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Přidat manuálně" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Manuell hinzufügen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Add manually" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Add manually" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Añadir manualmente" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Ajouter manuellement" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Add manually" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Aggiungi manualmente" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "수동 추가" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Dodaj ręcznie" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Adicionar manualmente" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Добавить вручную" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Elle ekle" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Додати вручну" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "手动添加" } } } }, "Add new visualizer" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Add new visualizer" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Přidat nový vizualizér" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Neuen Visualisierer hinzufügen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Add new visualizer" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Add new visualiser" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Añadir nuevo visualizador" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Ajouter un nouveau visualiseur" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Add new visualizer" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Aggiungi nuovo visualizzatore" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "새 시각화 도구 추가" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Dodaj nowy wizualizer" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Adicionar novo visualizador" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Добавить визуализатор" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Yeni görselleştirici ekle" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Додати новий візуалізатор" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "添加新的可视化器" } } } }, "Additional features" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Additional features" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Další funkce" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Zusätzliche Funktionen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Additional features" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Additional features" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Características adicionales" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Fonctionnalités supplémentaires" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Additional features" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Funzionalità aggiuntive" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "추가 기능들" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Dodatkowe funkcje" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Recursos adicionais" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Дополнительные возможности" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Ek özellikler" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Додаткові можливості" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "其他功能" } } } }, "Advanced" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Advanced" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Advanced" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Erweitert" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Advanced" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Advanced" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Advanced" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Advanced" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Advanced" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Advanced" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "고급" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Zaawansowane" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Advanced" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Advanced" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Advanced" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Advanced" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Advanced" } } } }, "All-day" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "All-day" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Celý den" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Ganztägig" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "All-day" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "All-day" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Todo el día" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Toute la journée" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "All-day" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Tutto Il Giorno" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "하루 종일" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Całodniowy" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Dia inteiro" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "All-day" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Gün boyu" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Увесь день" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "全天" } } } }, "Allow Access" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Allow Access" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Povolit přístup" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Zugriff erlauben" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Allow Access" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Allow Access" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Permitir acceso" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Autoriser l'accès" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Allow Access" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Consenti Accesso" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "권한 허용" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Zezwól na dostęp" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Permitir acesso" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Разрешить доступ" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Erişime izin ver" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Надати доступ" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "允许访问" } } } }, "Always show full event titles" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Always show full event titles" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Always show full event titles" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Vollständige Ereignistitel immer anzeigen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Always show full event titles" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Always show full event titles" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Always show full event titles" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Always show full event titles" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Always show full event titles" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Always show full event titles" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "항상 이벤트 제목 보이기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Zawsze pokazuj pełne tytuły wydarzeń" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Always show full event titles" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Always show full event titles" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Always show full event titles" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Always show full event titles" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Always show full event titles" } } } }, "Always show tabs" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Always show tabs" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Vždy zobrazovat panely" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Immer Tabs anzeigen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Always show tabs" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Always show tabs" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Mostrar siempre las pestañas" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Toujours afficher les onglets" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Always show tabs" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Mostra sempre schede" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "항상 탭 보이기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Zawsze pokazuj zakładki" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Permitir mostrar guias" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Всегда показывать вкладки" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Sekmeleri daima göster" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Завжди показувати вкладки" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "总是显示标签页" } } } }, "App icon" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "App icon" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Ikona aplikace" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "App Icon" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "App icon" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "App icon" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Icono de la aplicación" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Icône d'application" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "App icon" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Icona app" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "앱 아이콘" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Ikona aplikacji" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Ícone do App" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Значок приложения" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Uygulama simgesi" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Значок додатка" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "应用图标" } } } }, "Appearance" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Appearance" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Vzhled" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Darstellung" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Appearance" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Appearance" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Apariencia" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Apparence" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Appearance" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Aspetto" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "모양" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Wygląd" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Aparência" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Внешний вид" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Belirme" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Вигляд" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "外观" } } } }, "Auto-scroll to next event" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Auto-scroll to next event" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Auto-scroll to next event" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Automatisch zum nächsten Ereignis scrollen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Auto-scroll to next event" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Auto-scroll to next event" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Auto-scroll to next event" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Auto-scroll to next event" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Auto-scroll to next event" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Auto-scroll to next event" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "다음 이벤트로 오토-스크롤" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Automatyczne przewijanie do następnego wydarzenia" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Auto-scroll to next event" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Auto-scroll to next event" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Auto-scroll to next event" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Auto-scroll to next event" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Auto-scroll to next event" } } } }, "Automatically check for updates" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Automatically check for updates" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Automaticky vyhledávat aktualizace" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Automatisch nach Updates suchen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Automatically check for updates" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Automatically check for updates" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Comprobar actualizaciones automáticamente" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Vérifier les mises à jour automatiquement" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Automatically check for updates" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Controlla automaticamente gli aggiornamenti" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "자동으로 업데이트 확인" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Automatycznie sprawdzaj aktualizacje" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Procurar por atualizações automaticamente" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Автоматически проверять наличие обновлений" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Güncellemeleri otomatik kontrol et" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Автоматично перевіряти наявність оновлень" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "自动检查更新" } } } }, "Automatically download updates" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Automatically download updates" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Automaticky stahovat aktualizace" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Updates automatisch herunterladen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Automatically download updates" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Automatically download updates" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Descargar actualizaciones automáticamente" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Télécharger les mises à jour automatiquement" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Automatically download updates" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Scarica automaticamente gli aggiornamenti" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "자동으로 업데이트 다운로드" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Automatycznie pobieraj aktualizacje" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Baixar atualizações automaticamente" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Автоматически скачивать обновления" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Güncellemeleri otomatik indir" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Автоматично завантажувати оновлення" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "自动下载更新" } } } }, "Automatically switch displays" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Automatically switch displays" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Automaticky přepnout obrazovky" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Anzeige automatisch wechseln" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Automatically switch displays" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Automatically switch displays" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Cambiar automáticamente las pantallas" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Basculer automatiquement d'écran" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Automatically switch displays" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Cambiamento automatico display" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "디스플레이 자동 변경" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Automatycznie przełączaj wyświetlacze" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Trocar telas automaticamente" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Автоматически переключать экраны" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Ekranları otomatik olarak değiştir" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Автоматично перемикати екрани" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "自动切换显示器" } } } }, "Battery" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Battery" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Baterie" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Batterie" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Battery" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Battery" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Batería" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Batterie" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Battery" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Batteria" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "배터리" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Bateria" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Bateria" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Батарея" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Pil" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Акумулятор" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "电池" } } } }, "Battery Information" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Battery Information" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Informace o baterii" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Batterie Informationen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Battery Information" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Battery Information" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Información de la batería" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Informations de la batterie" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Battery Information" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Informazioni Batteria" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "배터리 정보" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Informacje o baterii" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Informações da Bateria" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Информация о батарее" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Pil bilgisi" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Інформація про акумулятор" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "电池信息" } } } }, "Battery Settings" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Battery Settings" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Nastavení baterie" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Batterie Einstellungen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Battery Settings" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Battery Settings" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Configuración de la batería" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Paramètres de la batterie" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Battery Settings" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Impostazioni Batteria" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "배터리 설정" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Ustawienia baterii" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Configurações da Bateria" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Параметры батареи" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Pil ayarları" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Налаштування акумулятора" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "电池设置" } } } }, "Battery Status" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Battery Status" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Stav baterie" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Batterie Status" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Battery Status" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Battery Status" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Estado de la batería" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Statut de la batterie" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Battery Status" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Stato Batteria" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "배터리 상태" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Stan baterii" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Status da bateria" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Состояние батареи" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Pil durumu" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Стан заряду акумулятора" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "电池状态" } } } }, "Boost your productivity with Clipboard Manager" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Boost your productivity with Clipboard Manager" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Zvyšte svou produktivitu pomocí správce schránky" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Steigere deine Produktivität mit dem Zwischenablage-Manager" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Boost your productivity with Clipboard Manager" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Boost your productivity with Clipboard Manager" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Aumenta tu productividad con Clipboard Manager" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Boostez votre productivité avec le gestionnaire de presse-papiers" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Boost your productivity with Clipboard Manager" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Incrementa la tua produttività con il gestore appunti" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "클립보드 매니저로 업무 효율을 높이세요" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Zwiększ swoją produktywność dzięki menedżerowi schowka" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Aumente sua produtividade com o Clipboard Manager" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Boost your productivity with Clipboard Manager" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Pano Yöneticisiyle üretkenliğinizi arttırın" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Підвищіть продуктивність за допомогою менеджера буфера обміну" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "使用剪贴板管理器提升你的效率" } } } }, "Boring Notch" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Boring Notch" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Boring Notch" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Boring Notch" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Boring Notch" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Boring Notch" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Boring Notch" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Boring Notch" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Boring Notch" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Boring Notch" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "Boring Notch" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Boring Notch" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Boring Notch" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Boring Notch" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Boring Notch" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Boring Notch" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Boring Notch" } } } }, "boring.notch" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "boring.notch" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "boring.notch" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "boring.notch" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "boring.notch" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "boring.notch" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "boring.notch" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "boring.notch" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "boring.notch" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "boring.notch" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "boring.notch" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "boring.notch" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "boring.notch" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "boring.notch" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "boring.notch" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "boring.notch" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "boring.notch" } } } }, "Calendar" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Calendar" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Kalendář" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Kalender" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Calendar" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Calendar" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Calendario" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Calendrier" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Calendar" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Calendario" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "캘린더" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Kalendarz" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Calendário" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Календарь" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Takvim" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Календар" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "日历" } } } }, "Calendar access is denied. Please enable it in System Settings." : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Calendar access is denied. Please enable it in System Settings." } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Přístup do kalendáře byl odepřen. Prosím povolte jej v nastavení systému." } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Kalender Zugriff verweigert. Bitte aktiviere es in den Systemeinstellungen." } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Calendar access is denied. Please enable it in System Settings." } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Calendar access is denied. Please enable it in System Settings." } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Acceso al calendario denegado. Por favor, habilítelo en Configuración del sistema." } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "L'accès au calendrier est refusé. Veuillez l'activer dans les réglages." } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Calendar access is denied. Please enable it in System Settings." } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "L'accesso al calendario è negato. Si prega di abilitarlo nelle impostazioni di sistema." } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "캘린더 권한 거부됨. 시스템 설정에서 허용해주세요.." } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Dostęp do kalendarza został zablokowany. Proszę odblokować dostęp w Ustawieniach Systemowych." } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Acesso ao Calendário negado. Por favor, autorize nas Configurações do Sistema." } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Доступ к календарю запрещен. Пожалуйста, включите его в Системных настройках." } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Takvim erişimi reddedildi. Lütfen Sistem Ayarlarından izin verin." } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Доступ до календаря заборонений. Будь ласка, увімкніть його в Налаштуваннях Системи." } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "日历访问被拒绝。请在系统设置中启用它。" } } } }, "Calendars" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Calendars" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Kalendáře" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Kalender" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Calendars" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Calendars" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Calendarios" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Calendriers" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Calendars" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Calendari" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "캘린더들" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Kalendarze" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Calendários" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Календари" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Takvimler" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Календарі" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "日历" } } } }, "Cancel" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Cancel" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Zrušit" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Abbrechen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Cancel" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Cancel" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Cancelar" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Annuler" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Cancel" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Annulla" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "취소" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Anuluj" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Cancelar" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Отмена" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "İptal et" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Скасувати" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "取消" } } } }, "Change media with horizontal gestures" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Change media with horizontal gestures" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Change media with horizontal gestures" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Medien mit horizontalen Gesten ändern" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Change media with horizontal gestures" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Change media with horizontal gestures" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Change media with horizontal gestures" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Change media with horizontal gestures" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Change media with horizontal gestures" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Change media with horizontal gestures" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "수평 제스처로 미디어 변경" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Zmień multimedia używając poziomych gestów" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Change media with horizontal gestures" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Change media with horizontal gestures" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Change media with horizontal gestures" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Change media with horizontal gestures" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Change media with horizontal gestures" } } } }, "Charging" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Charging" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Nabíjení" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Lädt" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Charging" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Charging" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Cargando" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "En charge" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Charging" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Caricamento" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "충전 중" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Ładowanie" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Carregando" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Charging" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Şarj oluyor" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Заряджається" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "充电中" } } } }, "Charging on Hold: Desktop Mode" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Charging on Hold: Desktop Mode" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Nabíjení zadrženo: Režim plochy" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Ladevorgang unterbrochen: Desktop-Modus" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Charging on Hold: Desktop Mode" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Charging on Hold: Desktop Mode" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Carga en Espera: Modo Escritorio" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Charge en pause : mode bureau" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Charging on Hold: Desktop Mode" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Caricamento in attesa: Modalità Desktop" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "충전 대기 중: 데스크탑 모드" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Ładowanie wstrzymane: Tryb stacjonarny" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Carregando em espera: Modo Desktop" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Charging on Hold: Desktop Mode" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Şarj beklemede: Masaüstü Modu" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Заряджання в Режимі очікування: Режим робочого столу" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "暂停充电:桌面模式" } } } }, "Check for Updates…" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Check for Updates…" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Zkontrolovat aktualizace…" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Suche nach Updates…" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Check for Updates…" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Check for Updates…" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Buscar actualizaciones…" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Vérifier les mises à jour…" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Check for Updates…" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Controlla aggiornamenti…" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "업데이트 확인…" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Sprawdź aktualizacje…" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Verificar atualizações…" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Проверить обновления…" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Güncellemeleri Kontrol et…" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Перевірка наявності оновлень…" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "检查更新…" } } } }, "Choose a Music Source" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose a Music Source" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Vyberte zdroj hudby" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Wähle eine Musikquelle" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Choose a Music Source" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Choose a Music Source" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Elige una Fuente de Música" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Choisir une source de musique" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose a Music Source" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Scegli una fonte musicale" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "음악 소스 선택" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Wybierz źródło muzyki" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Escolha fonte da música" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Выбрать источник музыки" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Müzik kaynağı seç" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Оберіть джерело музики" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "选择音乐来源" } } } }, "Choose any color" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose any color" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose any color" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Farbe auswählen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Choose any color" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose any colour" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose any color" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose any color" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose any color" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose any color" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "아무 색상을 선택해 주세요" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Wybierz dowolny kolor" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose any color" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose any color" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose any color" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose any color" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose any color" } } } }, "Choose between your system accent color or customize it with your own selection." : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose between your system accent color or customize it with your own selection." } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose between your system accent color or customize it with your own selection." } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Wählen Sie zwischen der Akzentfarbe Ihres Systems oder passen Sie sie mit Ihrer eigenen Auswahl an." } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Choose between your system accent color or customize it with your own selection." } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose between your system accent colour or customize it with your own selection." } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose between your system accent color or customize it with your own selection." } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose between your system accent color or customize it with your own selection." } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose between your system accent color or customize it with your own selection." } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose between your system accent color or customize it with your own selection." } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "시스템 강조 색상을 사용하거나 직접 색상을 선택해 설정하세요." } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Wybierz kolor akcentu systemowego lub dostosuj go według własnego wyboru." } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose between your system accent color or customize it with your own selection." } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose between your system accent color or customize it with your own selection." } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose between your system accent color or customize it with your own selection." } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose between your system accent color or customize it with your own selection." } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "" } } } }, "Choose which service to use when sharing files from the shelf. Click the shelf button to select files, or drag files onto it to share immediately." : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose which service to use when sharing files from the shelf. Click the shelf button to select files, or drag files onto it to share immediately." } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose which service to use when sharing files from the shelf. Click the shelf button to select files, or drag files onto it to share immediately." } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Wählen Sie den zu verwendenden Dienst aus, wenn Dateien geteilt werden. Klicken Sie auf die Ablage-Taste, um Dateien auszuwählen oder ziehen Sie Dateien auf die Ablage, um sie sofort zu teilen." } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Choose which service to use when sharing files from the shelf. Click the shelf button to select files, or drag files onto it to share immediately." } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose which service to use when sharing files from the shelf. Click the shelf button to select files, or drag files onto it to share immediately." } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose which service to use when sharing files from the shelf. Click the shelf button to select files, or drag files onto it to share immediately." } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose which service to use when sharing files from the shelf. Click the shelf button to select files, or drag files onto it to share immediately." } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose which service to use when sharing files from the shelf. Click the shelf button to select files, or drag files onto it to share immediately." } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose which service to use when sharing files from the shelf. Click the shelf button to select files, or drag files onto it to share immediately." } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "선반에서 파일을 공유할 때 사용할 서비스를 선택하세요. 선반 버튼을 클릭해 파일을 선택하거나, 파일을 끌어다 놓으면 바로 공유할 수 있습니다." } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Wybierz usługę używaną podczas udostępniania plików z półki. Kliknij przycisk półki, aby wybrać pliki, lub przeciągnij pliki na niego, aby udostępnić natychmiast." } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose which service to use when sharing files from the shelf. Click the shelf button to select files, or drag files onto it to share immediately." } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose which service to use when sharing files from the shelf. Click the shelf button to select files, or drag files onto it to share immediately." } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose which service to use when sharing files from the shelf. Click the shelf button to select files, or drag files onto it to share immediately." } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose which service to use when sharing files from the shelf. Click the shelf button to select files, or drag files onto it to share immediately." } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Choose which service to use when sharing files from the shelf. Click the shelf button to select files, or drag files onto it to share immediately." } } } }, "Circle" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Circle" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Kruh" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Kreis" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Circle" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Circle" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Círculo" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Cercle" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Circle" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Cerchio" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "원형의" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Okręg" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Círculo" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Circle" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Çember" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Коло" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "圆形" } } } }, "Clear slot" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Clear slot" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Clear slot" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Slot leeren" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Clear slot" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Clear slot" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Clear slot" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Clear slot" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Clear slot" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Clear slot" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "슬롯 초기화" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Wyczyść slot" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Clear slot" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Clear slot" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Clear slot" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Clear slot" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Clear slot" } } } }, "Close" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "قُفْل" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Zavřít" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Schließen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Close" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Close" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Cerrar" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Fermer" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Close" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Chiudi" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "닫기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Zamknij" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Fechar" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Close" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Kapat" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Закрити" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "关闭" } } } }, "Close gesture" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Close gesture" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Gesto zavření" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Schließgeste" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Close gesture" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Close gesture" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Gesto de cerrar" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Geste de fermeture" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Close gesture" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Gesto di chiusura" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "닫기 제스처" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Gest zamknięcia" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Close gesture" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Close gesture" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "El hareketlerini kapat" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Жест вимкнення" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "关闭手势" } } } }, "Closed Notch" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Closed Notch" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Closed Notch" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Closed Notch" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Closed Notch" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Closed Notch" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Closed Notch" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Closed Notch" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Closed Notch" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Closed Notch" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "Closed Notch" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Closed Notch" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Closed Notch" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Closed Notch" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Closed Notch" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Closed Notch" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Closed Notch" } } } }, "Color Presets" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Color Presets" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Color Presets" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Farbvorgaben" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Color Presets" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Colour Presets" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Color Presets" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Color Presets" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Color Presets" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Color Presets" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "색상 프리셋들" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Predefiniowane kolory" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Color Presets" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Color Presets" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Color Presets" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Color Presets" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Color Presets" } } } }, "Colored spectrogram" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Colored spectrogram" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Colored spectrogram" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Farbiges Spektrogramm" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Colored spectrogram" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Coloured spectrogram" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Colored spectrogram" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Colored spectrogram" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Colored spectrogram" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Colored spectrogram" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "컬러 스펙트로그램" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Kolorowy spektrogram" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Colored spectrogram" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Colored spectrogram" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Colored spectrogram" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Colored spectrogram" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Colored spectrogram" } } } }, "Coming soon" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Coming soon" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Již brzy" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Demnächst" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Coming soon" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Coming soon" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Próximamente" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Bientôt disponible" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Coming soon" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "In arrivo" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "곧 출시" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Już wkrótce" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Em breve" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Coming soon" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Çok yakında geliyor" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Незабаром" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "即将上线" } } } }, "Continue" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Continue" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Pokračovat" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Fortfahren" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Continue" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Continue" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Continuar" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Continuer" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Continue" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Continua" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "계속" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Kontynuuj" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Continue" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Продолжить" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Devam Et" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Далі" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "继续" } } } }, "Copy items on drag" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Copy items on drag" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Copy items on drag" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Elemente beim Draufziehen kopieren" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Copy items on drag" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Copy items on drag" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Copy items on drag" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Copy items on drag" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Copy items on drag" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Copy items on drag" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "드래그로 항목 복사" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Kopiuj elementy po przeciągnięciu" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Copy items on drag" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Copy items on drag" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Copy items on drag" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Copy items on drag" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Copy items on drag" } } } }, "Corner radius scaling" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Corner radius scaling" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Corner radius scaling" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Skalierung Eckenradius" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Corner radius scaling" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Corner radius scaling" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Escalado del radio de las esquinas" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Redimensionnement du rayon des angles" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Corner radius scaling" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Scala raggio angoli" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "모서리 반경 크기 조정" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Skalowanie zaokrąglenia narożników" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Corner radius scaling" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Corner radius scaling" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Köşe yarıçap ölçeği" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Масштабування радіусу кутів" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "圆角半径" } } } }, "Currently selected: %@" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Currently selected: %@" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Currently selected: %@" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Derzeit ausgewählt: %@" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Currently selected: %@" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Currently selected: %@" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Currently selected: %@" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Currently selected: %@" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Currently selected: %@" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Currently selected: %@" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "현재 선택된: %@" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Aktualnie wybrane: %@" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Currently selected: %@" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Currently selected: %@" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Currently selected: %@" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Currently selected: %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Currently selected: %@" } } } }, "Custom" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Custom" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Custom" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Benutzerdefiniert" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Custom" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Custom" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Custom" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Custom" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Custom" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Custom" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "사용자정의" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Niestandardowe" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Custom" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Custom" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Custom" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Custom" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Custom" } } } }, "Custom height" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Custom height" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Vlastní výška" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Benutzerdefinierte Höhe" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Custom height" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Custom height" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Altura personalizada" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Personnaliser la hauteur" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Custom height" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Altezza personalizzata" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "사용자 정의 높이" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Niestandardowa wysokość" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Customizar altura" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Custom height" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Özel yükseklik" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Користувацька висота" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "自定义高度" } } } }, "Custom music live activity animation" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Custom music live activity animation" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Vlastní animace živé aktivity hudby" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Benutzerdefinierte Animation für Musik-Live-Aktivitäten" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Custom music live activity animation" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Custom music live activity animation" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Animación de actividad de música en vivo personalizada" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Personnaliser l'animation d'activité musicale" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Custom music live activity animation" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Animazione attività musicale personalizzata" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "사용자 정의 음악 재생 애니메이션" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Niestandardowa animacja aktywności muzyki" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Customizar animação da música" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Custom music live activity animation" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Özel canlı müzik animasyonu" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Власна анімація музики в реальному часі" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "自定义音乐实时动画" } } } }, "Custom notch size - %.0f" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Custom notch size - %.0f" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Vlastní velikost výřezu - %.0f" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Benutzerdefinierte Notchgröße - %.0f" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Custom notch size - %.0f" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Custom notch size - %.0f" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Tamaño personalizado del notch - %.0f" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Personnaliser la taille de l'encoche - %.0f" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Custom notch size - %.0f" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Dimensione notch personalizzata - %.0f" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "사용자 정의 노치 사이즈 - %.0f" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Niestandardowy rozmiar notcha - %.0f" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Descrição" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Custom notch size - %.0f" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Özel çentik boyutu - %.0f" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Користувацький розмір брові - %.0f" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "自定义Notch大小 - %.0f" } } } }, "Custom vizualizers (Lottie)" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Custom vizualizers (Lottie)" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Vlastní vizualizátory (Lottie)" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Eigene Audiovisualisierung (via Lottie)" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Custom vizualizers (Lottie)" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Custom visualisers (Lottie)" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Visualizadores personalizados (Lottie)" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Visualiseurs personnalisés (Lottie)" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Custom vizualizers (Lottie)" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Visualizzatori personalizzati (Lottie)" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "사용자 지정 시각화 도구(Lottie)" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Niestandardowe wizualizatory (Lottie)" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Customizar visualizadores (Lottie)" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Custom vizualizers (Lottie)" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Özel görselleştirmeler (Lottie)" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Користувацькі візуалізатори (Лотті)" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "自定义可视化器(Lottie)" } } } }, "Customize in Settings" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Customize in Settings" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Přizpůsobit v nastavení" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "In den Einstellungen anpassen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Customize in Settings" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Customise in Settings" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Personalizar en Ajustes" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Personnaliser dans les paramètres" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Customize in Settings" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Personalizza nelle impostazioni" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "설정에서 커스텀하기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Dostosuj w ustawieniach" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Customizar em Configurações" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Customize in Settings" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Ayarlarda özelleştir" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Змінити в Налаштуваннях" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "在设置中自定义" } } } }, "Customize which controls appear in the music player. Volume expands when active." : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Customize which controls appear in the music player. Volume expands when active." } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Customize which controls appear in the music player. Volume expands when active." } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Passen Sie an, welche Steuerelemente im Musik-Player angezeigt werden. Die Lautstärke wird erweitert, wenn aktiviert." } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Customize which controls appear in the music player. Volume expands when active." } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Customize which controls appear in the music player. Volume expands when active." } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Customize which controls appear in the music player. Volume expands when active." } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Customize which controls appear in the music player. Volume expands when active." } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Customize which controls appear in the music player. Volume expands when active." } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Customize which controls appear in the music player. Volume expands when active." } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "음악 플레이어에 표시할 컨트롤을 사용자 지정하세요. 볼륨은 활성화되면 확장됩니다." } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Dostosuj które ustawienia wyświetlane w odtwarzaczu muzyki. Głośność rozwija się, gdy jest aktywna." } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Customize which controls appear in the music player. Volume expands when active." } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Customize which controls appear in the music player. Volume expands when active." } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Customize which controls appear in the music player. Volume expands when active." } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Customize which controls appear in the music player. Volume expands when active." } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Customize which controls appear in the music player. Volume expands when active." } } } }, "Default" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Default" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Výchozí" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Standard" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Default" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Default" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Por defecto" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Par défaut" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Default" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Predefinito" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "기본" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Domyślny" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Padrão" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Default" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Varsayılan" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Типово" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "默认" } } } }, "Description" : { "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Description" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Popis" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Beschreibung" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Description" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Description" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Descripción" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Description" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Description" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Descrizione" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "설명" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Opis" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Descrição" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Description" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Açıklama" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Опис" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "描述" } } } }, "Disable" : { "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Disable" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Vypnout" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Deaktivieren" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Disable" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Disable" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Deshabilitar" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Désactiver" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Disable" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Disabilita" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "비활성화" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Wyłącz" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Disable" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Disable" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Devre dışı bırak" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Вимкнути" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "禁用" } } } }, "Disabled" : { "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Disabled" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Vypnuto" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Deaktiviert" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Disabled" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Disabled" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Deshabilitado" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Désactivé" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Disabled" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Disabilitato" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "비활성화됨" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Wyłączone" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Disabled" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Disabled" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Devre dışı bırakıldı" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Вимкнено" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "已禁用" } } } }, "Download" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Download" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Stáhnout" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Herunterladen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Download" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Download" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Descargar" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Télécharger" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Download" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Scarica" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "다운로드" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Pobierz" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Download" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Download" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "İndir" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Завантажити" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "下载" } } } }, "Downloads" : { "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Downloads" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Stahování" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Downloads" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Downloads" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Downloads" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Descargas" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Téléchargements" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Downloads" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Scaricati" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "다운로드" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Pobrane" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Downloads" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Downloads" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "İndirilenler" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Завантаження" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "下载" } } } }, "Drag a control onto a slot" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Drag a control onto a slot" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Drag a control onto a slot" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Ziehen Sie ein Steuerelement auf einen Slot" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Drag a control onto a slot" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Drag a control onto a slot" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Drag a control onto a slot" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Drag a control onto a slot" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Drag a control onto a slot" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Drag a control onto a slot" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "컨트롤을 슬롯으로 끌어다 놓으세요." } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Przeciągnij sterowanie na slot" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Drag a control onto a slot" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Drag a control onto a slot" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Drag a control onto a slot" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Drag a control onto a slot" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Drag a control onto a slot" } } } }, "Drag items in the preview to reorder or drop from the palette" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Drag items in the preview to reorder or drop from the palette" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Drag items in the preview to reorder or drop from the palette" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Ziehen Sie Elemente in die Vorschau, um sie neu zu ordnen oder aus der Palette zu ziehen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Drag items in the preview to reorder or drop from the palette" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Drag items in the preview to reorder or drop from the palette" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Drag items in the preview to reorder or drop from the palette" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Drag items in the preview to reorder or drop from the palette" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Drag items in the preview to reorder or drop from the palette" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Drag items in the preview to reorder or drop from the palette" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "미리보기에서 항목을 끌어 순서를 변경하거나 팔레트에서 끌어다 놓으세요." } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Przeciągnij elementy w podglądzie, aby zmienić kolejność lub upuścić z palety" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Drag items in the preview to reorder or drop from the palette" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Drag items in the preview to reorder or drop from the palette" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Drag items in the preview to reorder or drop from the palette" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Drag items in the preview to reorder or drop from the palette" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Drag items in the preview to reorder or drop from the palette" } } } }, "Drop files here" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Drop files here" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Přetáhněte soubory sem" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Dateien hier ablegen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Drop files here" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Drop files here" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Suelte los archivos aquí" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Déposer les fichiers ici" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Drop files here" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Trascina i file qui" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "여기로 파일을 드래그하세요" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Upuść pliki tutaj" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Drop files here" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Drop files here" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Dosyaları buraya bırak" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Перетягніть файл сюди" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "拖放文件到这里" } } } }, "Easily copy, store, and manage your most-used content. Upgrade now for advanced features like multi-item storage and quick access!" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Easily copy, store, and manage your most-used content. Upgrade now for advanced features like multi-item storage and quick access!" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Snadno kopírujte, ukládejte a spravujte svůj nejpoužívanejší obsah. Upgradujte nyní pro pokročilé funkce, jako je vícenásobné úložiště a rychlý přístup!" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Kopieren, speichern und verwalten Sie ganz einfach Ihre am häufigsten verwendeten Inhalte. Aktualisieren Sie jetzt, um erweiterte Funktionen wie die Speicherung mehrerer Elemente und schnellen Zugriff zu erhalten!" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Easily copy, store, and manage your most-used content. Upgrade now for advanced features like multi-item storage and quick access!" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Easily copy, store, and manage your most-used content. Upgrade now for advanced features like multi-item storage and quick access!" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Copie, almacene y gestione fácilmente su contenido más usado. ¡Actualice ahora para obtener características avanzadas como almacenamiento de múltiples elementos y acceso rápido!" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Copiez, stockez et gérez facilement votre contenu le plus utilisé. Mettez à niveau maintenant pour des fonctionnalités avancées telles que le stockage de multiples items et un accès rapide !" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Easily copy, store, and manage your most-used content. Upgrade now for advanced features like multi-item storage and quick access!" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Copia facilmente, memorizza e gestisci i tuoi contenuti più usati. Aggiorna ora per funzioni avanzate come storage multi-item e accesso rapido!" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "자주 사용하는 파일들을 쉽게 복사, 저장, 관리하세요. 업그레이드 해서 다중 저장 기능과 빠른 접근 기능을 활성화하세요!" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Łatwo kopiuj, przechowuj i zarządzaj najczęściej używaną treścią. Uaktualnij teraz, aby uzyskać zaawansowane funkcje, takie jak przechowywanie wielu produktów i szybki dostęp!" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Easily copy, store, and manage your most-used content. Upgrade now for advanced features like multi-item storage and quick access!" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Easily copy, store, and manage your most-used content. Upgrade now for advanced features like multi-item storage and quick access!" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "En sık kullandığınız içerikleri kolayca kopyalayın, saklayın ve yönetin. Çoklu öğe depolama ve hızlı erişim gibi gelişmiş özellikler için şimdi yükseltin!" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Easily copy, store, and manage your most-used content. Upgrade now for advanced features like multi-item storage and quick access!" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "轻松复制、存储和管理您最常用的内容。升级后可解锁高级功能,如多项目存储和快速访问!" } } } }, "Edit layout" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Edit layout" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Upravit rozložení" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Layout bearbeiten" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Edit layout" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Edit layout" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Editar diseño" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Modifier la mise en page" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Edit layout" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Modifica layout" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "레이아웃 수정" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Edytuj układ" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Edit layout" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Edit layout" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Yerleşimi düzenle" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Редагувати розташування" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "编辑布局" } } } }, "Enable blur effect behind album art" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable blur effect behind album art" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Povolit efekt rozostření za obalem alba" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Unschärfe-Effekt hinter Albumcover aktivieren" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Enable blur effect behind album art" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Enable blur effect behind album art" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Habilitar el efecto de difuminado detrás de la portada del álbum" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Activer l'effet de flou derrière la pochette d'album" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable blur effect behind album art" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Abilita sfocatura dietro alla copertina dell'album" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "앨범 커버 뒤 블러효과 켜기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Włącz efekt rozmycia za okładką albumu" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable blur effect behind album art" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable blur effect behind album art" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Albüm kapağının arkasındaki bulanıklık efektini aktifleştir" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Увімкнути ефект розмиття позаду обкладинки альбому" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "启用专辑封面的模糊效果" } } } }, "Enable boring mirror" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable boring mirror" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Povolit boring zrcadlo" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Spiegelfunktion aktivieren" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Enable boring mirror" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Enable boring mirror" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Habilitar espejo boring" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Activer le miroir ennuyeux" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable boring mirror" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Abilita specchio" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "Boring 거울 켜기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Włącz boring mirror" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable boring mirror" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable boring mirror" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Sıkıcı aynayı aktifleştir" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable boring mirror" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "启用虚拟镜像" } } } }, "Enable colored spectrograms" : { "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable colored spectrograms" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Povolit barevné spektrogramy" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Farbe für Audiovisualisierung aktivieren" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Enable colored spectrograms" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Enable coloured spectrograms" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Habilitar espectrogramas de colores" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Activer les spectrogrammes de couleurs" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable colored spectrograms" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Abilita spettrogramma colorato" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "컬러 스펙트로그램을 활성화하세요" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Włącz kolorowe spektrogramy" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable colored spectrograms" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable colored spectrograms" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Renkli spektrogramları aktifleştir" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable colored spectrograms" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "启用彩色谱图" } } } }, "Enable gestures" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable gestures" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Povolit gesta" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Gestensteuerung aktivieren" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Enable gestures" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Enable gestures" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Habilitar gestos" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Activer les gestes" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable gestures" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Abilita gesti" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "제스처 켜기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Włącz obsługę gestów" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable gestures" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable gestures" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "El hareketlerini aktifleştir" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Увімкнути жести" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "启用手势" } } } }, "Enable glowing effect" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable glowing effect" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable glowing effect" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Leuchteffekt aktivieren" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Enable glowing effect" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Enable glowing effect" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Habilitar efecto de resplandor" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Activer l'effet lumineux" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable glowing effect" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Abilita effetto luminoso" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "발광 효과 켜기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Włącz efekt poświaty" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable glowing effect" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable glowing effect" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Parlama efektini aktifleştir" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable glowing effect" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "启用发光效果" } } } }, "Enable haptic feedback" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable haptic feedback" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable haptic feedback" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Haptisches Feedback aktivieren" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Enable haptic feedback" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable haptic feedback" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable haptic feedback" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable haptic feedback" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable haptic feedback" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable haptic feedback" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "햅틱 피드백 활성화" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Włącz informację zwrotną w postaci wibracji" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable haptic feedback" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable haptic feedback" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable haptic feedback" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable haptic feedback" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable haptic feedback" } } } }, "Enable shelf" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable shelf" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Povolit polici" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Ablage aktivieren" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Enable shelf" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Enable shelf" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Habilitar bandeja" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Activer la bibliothèque" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable shelf" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Abilita scaffale" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "서랍 켜기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Włącz półkę" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable shelf" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable shelf" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Rafı aktifleştir" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Активувати полицю" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "启用暂存区" } } } }, "Enable window shadow" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable window shadow" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable window shadow" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Fensterschatten aktivieren" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Enable window shadow" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Enable window shadow" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Habilitar sombra de la ventana" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Activer l'ombre de la fenêtre" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable window shadow" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Abilita ombra della finestra" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "윈도우 그림자 켜기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Włącz cień okna" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable window shadow" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable window shadow" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Pencere gölgesini aktifleştir" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Enable window shadow" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "启用窗口阴影" } } } }, "Enhance your experience with HUDs" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Enhance your experience with HUDs" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Vylepšete své zkušenosti s HUDy" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Verbessere deine Erfahrung mit HUDs" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Enhance your experience with HUDs" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Enhance your experience with HUDs" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Mejora tu experiencia con estilos de información en pantalla" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Améliorez votre expérience avec les HUD" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Enhance your experience with HUDs" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Migliora la tua esperienza con gli HUD" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "HUD로 경험을 향상 하세요" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Ulepsz swoje doświadczenia z HUD" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Enhance your experience with HUDs" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Enhance your experience with HUDs" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "BÜG ile deneyiminizi artırın" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Enhance your experience with HUDs" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "用 HUD 提升您的体验" } } } }, "Enjoy your free time!" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Enjoy your free time!" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Užijte si svůj volný čas!" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Genieße deine Freizeit!" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Enjoy your free time!" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Enjoy your free time!" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "¡Disfruta de tu tiempo libre!" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Profitez de votre temps libre !" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Enjoy your free time!" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Goditi il tuo tempo libero!" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "즐거운 시간 되세요!" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Ciesz się swoim wolnym czasem!" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Enjoy your free time!" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Enjoy your free time!" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Kıymetli vaktinizin keyfini çıkarın!" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Насолоджуйтесь своїм вільним часом!" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "享受您的闲暇时光!" } } } }, "Expanded drag detection area" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Expanded drag detection area" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Expanded drag detection area" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Erweiterter Ziehen Erkennungsbereich" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Expanded drag detection area" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Expanded drag detection area" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Expanded drag detection area" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Expanded drag detection area" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Expanded drag detection area" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Expanded drag detection area" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "확장된 드래그 감지 영역" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Rozszerzony obszar wykrywania przeciągnięcia" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Expanded drag detection area" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Expanded drag detection area" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Expanded drag detection area" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Expanded drag detection area" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Expanded drag detection area" } } } }, "Extend hover area" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Extend hover area" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Extend hover area" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Bereich für Mausberührung erweitern" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Extend hover area" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Extend hover area" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Extender la zona de activación del cursor" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Étendre la zone de survol" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Extend hover area" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Estendi area di attivazione col mouse" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "갖다대기 범위 늘이기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Rozszerz obszar najechania kursorem" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Extend hover area" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Extend hover area" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Sürükleme alanını genişlet" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Extend hover area" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "扩展悬停区域" } } } }, "Extensions" : { "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Extensions" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Rozšíření" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Erweiterungen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Extensions" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Extensions" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Extensiones" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Extensions" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Extensions" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Estensioni" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "확장기능들" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Rozszerzenia" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Extensions" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Extensions" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Genişletmeler" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Розширення" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "扩展" } } } }, "Files dropped on the shelf will be shared via this service" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Files dropped on the shelf will be shared via this service" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Files dropped on the shelf will be shared via this service" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Dateien, die in der Ablage abgelegt werden, werden über diesen Dienst geteilt" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Files dropped on the shelf will be shared via this service" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Files dropped on the shelf will be shared via this service" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Files dropped on the shelf will be shared via this service" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Files dropped on the shelf will be shared via this service" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Files dropped on the shelf will be shared via this service" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Files dropped on the shelf will be shared via this service" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "선반에 놓인 파일은 이 서비스를 통해 공유됩니다." } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Pliki upuszczone na półkę będą udostępniane za pośrednictwem tej usługi" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Files dropped on the shelf will be shared via this service" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Files dropped on the shelf will be shared via this service" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Files dropped on the shelf will be shared via this service" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Files dropped on the shelf will be shared via this service" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Files dropped on the shelf will be shared via this service" } } } }, "Finish" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Finish" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Dokončit" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Beenden" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Finish" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Finish" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Finalizar" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Terminer" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Finish" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Fine" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "완료" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Zakończ" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Finish" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Finish" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Bitir" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Завершити" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "完成" } } } }, "Full screen behavior" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Full screen behavior" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Full screen behavior" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Vollbild-Verhalten" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Full screen behavior" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Full screen behavior" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Full screen behavior" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Full screen behavior" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Full screen behavior" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Full screen behavior" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "전체 화면 동작" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Zachowanie na pełnym ekranie" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Full screen behavior" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Full screen behavior" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Full screen behavior" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Full screen behavior" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Full screen behavior" } } } }, "General" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "General" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Obecné" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Allgemein" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "General" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "General" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "General" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Général" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "General" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Generali" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "일반" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Ogólne" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "General" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "General" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Genel" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "General" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "通用" } } } }, "Gesture control" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Gesture control" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Kontrola gest" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Gestensteuerung" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Gesture control" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Gesture control" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Control de gestos" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Contrôle gestuel" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Gesture control" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Controllo gesti" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "제스처 컨트롤" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Ustawienia gestów" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Gesture control" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Gesture control" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Hareketle Kontrol" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Керування жестами" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "手势控制" } } } }, "Gesture sensitivity" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Gesture sensitivity" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Citlivost gesta" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Gesten Empfindlichkeit" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Gesture sensitivity" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Gesture sensitivity" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Sensibilidad del gesto" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Sensibilité de la gestuelle" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Gesture sensitivity" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Sensibilità gesti" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "제스처 민감도" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Czułość gestów" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Gesture sensitivity" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Gesture sensitivity" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Hareket hassasiyeti" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Чутливість жестів" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "手势灵敏度" } } } }, "Get started" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Get started" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Začněte" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Erste Schritte" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Get started" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Get started" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Comenzar" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Commencez" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Get started" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Inizia" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "시작하기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Rozpocznij" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Get started" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Get started" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Başlayın" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Get started" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "开始使用" } } } }, "GitHub" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "GitHub" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "GitHub" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "GitHub" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "GitHub" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "GitHub" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "GitHub" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "GitHub" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "GitHub" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "GitHub" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "GitHub" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "GitHub" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "GitHub" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "GitHub" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "GitHub" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "GitHub" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "GitHub" } } } }, "Got it!" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Got it!" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Mám to!" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Verstanden!" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Got it!" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Got it!" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "¡Entendido!" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Compris !" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Got it!" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Capito!" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "알겠습니다!" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Rozumiem!" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Got it!" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Got it!" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Anlaşıldı!" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Got it!" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "明白了!" } } } }, "Gradient" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Gradient" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Přechod" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Farbverlauf" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Gradient" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Gradient" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Gradiente" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Dégradé" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Gradient" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Sfumatura" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "그라디언트" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Gradient" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Gradiente" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Gradient" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Gradyan" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Градієнт" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "渐变" } } } }, "Hide all-day events" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide all-day events" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide all-day events" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Ganztägige Ereignisse ausblenden" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Hide all-day events" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide all-day events" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide all-day events" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide all-day events" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide all-day events" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide all-day events" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "종일 이벤트를 숨기기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Ukryj wydarzenia całodniowe" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide all-day events" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide all-day events" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide all-day events" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide all-day events" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide all-day events" } } } }, "Hide completed reminders" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide completed reminders" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Schovat vyřízené připomínky" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Erledigte Erinnerungen verbergen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Hide completed reminders" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Hide completed reminders" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Ocultar recordatorios completados" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Cacher les rappels terminés" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide completed reminders" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "완료된 알림 숨기기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Ukryj zakończone przypomnienia" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide completed reminders" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide completed reminders" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Tamamlanan Hatırlatıcıları gizle" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Приховати завершені нагадування" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "隐藏已完成提醒" } } } }, "Hide for all apps" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide for all apps" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide for all apps" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Für alle Apps ausblenden" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Hide for all apps" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide for all apps" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide for all apps" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide for all apps" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide for all apps" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide for all apps" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "모든 앱에서 숨기기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Ukryj dla wszystkich aplikacji" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide for all apps" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide for all apps" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide for all apps" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide for all apps" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide for all apps" } } } }, "Hide for media app only" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide for media app only" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide for media app only" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Nur für Medien-App ausblenden" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Hide for media app only" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide for media app only" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide for media app only" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide for media app only" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide for media app only" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide for media app only" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "미디어 앱에서만 숨기기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Ukryj tylko dla aplikacji multimediów" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide for media app only" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide for media app only" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide for media app only" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide for media app only" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide for media app only" } } } }, "Hide from screen recording" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide from screen recording" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide from screen recording" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Bei Bildschirmaufnahmen ausblenden" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Hide from screen recording" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide from screen recording" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide from screen recording" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide from screen recording" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide from screen recording" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide from screen recording" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "화면 녹화 시 숨기기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Ukryj przed nagrywaniem ekranu" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide from screen recording" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide from screen recording" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide from screen recording" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide from screen recording" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide from screen recording" } } } }, "Hide title bar" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide title bar" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide title bar" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Titelleiste ausblenden" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Hide title bar" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide title bar" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide title bar" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide title bar" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide title bar" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide title bar" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "제목 표시줄 숨기기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Ukryj pasek tytułu" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide title bar" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide title bar" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide title bar" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide title bar" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Hide title bar" } } } }, "Hierarchical" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Hierarchical" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Hierarchické" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Hierarchisch" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Hierarchical" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Hierarchical" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Jerárquico" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Hiérarchique" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Hierarchical" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Gerarchico" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "계층적" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Hierarchiczny" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Hierarchical" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Hierarchical" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Hiyerarşik" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Hierarchical" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "分级" } } } }, "High" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "High" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Vysoká" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Hoch" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "High" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "High" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Alto" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Haut" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "High" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Alta" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "높음" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Wysoki" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "High" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "High" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Yüksek" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Високий" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "高" } } } }, "Hover delay" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Hover delay" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Hover delay" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Schwebeverzögerung" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Hover delay" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Hover delay" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Hover delay" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Hover delay" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Hover delay" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Hover delay" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "호버 지연 시간" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Opóźnienie po najechaniu" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Hover delay" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Hover delay" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Hover delay" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Hover delay" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Hover delay" } } } }, "https://github.com/pear-devs/pear-desktop" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "https://github.com/pear-devs/pear-desktop" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "https://github.com/pear-devs/pear-desktop" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "https://github.com/pear-devs/pear-desktop" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "https://github.com/pear-devs/pear-desktop" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "https://github.com/pear-devs/pear-desktop" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "https://github.com/pear-devs/pear-desktop" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "https://github.com/pear-devs/pear-desktop" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "https://github.com/pear-devs/pear-desktop" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "https://github.com/pear-devs/pear-desktop" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "https://github.com/pear-devs/pear-desktop" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "https://github.com/pear-devs/pear-desktop" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "https://github.com/pear-devs/pear-desktop" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "https://github.com/pear-devs/pear-desktop" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "https://github.com/pear-devs/pear-desktop" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "https://github.com/pear-devs/pear-desktop" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "https://github.com/pear-devs/pear-desktop" } } } }, "HUD style" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "HUD style" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Styl HUD" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "HUD Stil" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "HUD style" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "HUD style" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Estilo de la información en pantalla" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Style de l'HUD" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "HUD style" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Stile HUD" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "HUD 스타일" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Styl HUD" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "HUD style" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "HUD style" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "BÜG stili" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "HUD style" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "HUD 样式" } } } }, "HUDs" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "HUDs" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "HUDy" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "HUDs" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "HUDs" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "HUDs" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Estilos de información en pantalla" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "HUDs" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "HUDs" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "HUDs" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "HUD들" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Interfejsy HUD" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "HUDs" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "HUDs" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "BÜG (Baş Üstü Göstergesi)" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "HUDs" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "HUD" } } } }, "In progress" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "In progress" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Probíhá" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "In Bearbeitung" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "In progress" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "In progress" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "En progreso" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "En cours" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "In progress" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "In corso" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "진행 중" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "W toku" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "In progress" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "İşleniyor" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "В процесі" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "进行中" } } } }, "Inline" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Inline" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "V řadě" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Inline" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Inline" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Inline" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "En Línea" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Inline" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Inline" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "In linea" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "인라인" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Wbudowany" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Em linha" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Inline" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Hiza" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Inline" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "内嵌" } } } }, "Installed extensions" : { "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Installed extensions" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Nainstalovaná rozšíření" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Installierte Erweiterungen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Installed extensions" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Installed extensions" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Extensiones instaladas" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Extensions installées" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Installed extensions" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Estensioni installate" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "설치된 확장기능들" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Zainstalowane rozszerzenia" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Installed extensions" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Installed extensions" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Yüklenmiş eklentiler" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Встановлені розширення" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "已安装扩展" } } } }, "Layout Preview" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Layout Preview" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Layout Preview" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Layoutvorschau" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Layout Preview" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Layout Preview" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Layout Preview" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Layout Preview" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Layout Preview" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Layout Preview" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "레이아웃 미리보기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Podgląd układu" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Layout Preview" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Layout Preview" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Layout Preview" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Layout Preview" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Layout Preview" } } } }, "Lottie JSON URL" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Lottie JSON URL" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Lottie JSON URL" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Lottie JSON URL" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Lottie JSON URL" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Lottie JSON URL" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Lottie JSON URL" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Lien URL Lottie JSON" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Lottie JSON URL" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "URL Lottie JSON" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "Lottie JSON URL" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Lottie JSON URL" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Lottie JSON URL" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Lottie JSON URL" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Lottie JSON URL" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Lottie JSON URL" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Lottie JSON 链接" } } } }, "Low" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Low" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Nízká" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Niedrig" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Low" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Low" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Bajo" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Bas" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Low" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Bassa" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "낮음" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Niski" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Low" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Low" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Düşük" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Low" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "低" } } } }, "Low Power Mode" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Low Power Mode" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Režim nízké spotřeby" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Energiesparmodus" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Low Power Mode" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Low Power Mode" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Modo de bajo consumo" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Mode économie d'énergie" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Low Power Mode" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Modalità Bassa Potenza" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "저전력 모드" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Tryb niskiego zużycia energii" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Low Power Mode" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Low Power Mode" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Düşük Güç Modu" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Режим економії енергії" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "低电量模式" } } } }, "Made with 🫶🏻 by not so boring not.people" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Made with 🫶🏻 by not so boring not.people" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Vyrobeno s 🫶🏻 ne tak nudnými ne lidmi" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Gemacht mit 🫶🏻 durch nicht so boring not.people" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Made with 🫶🏻 by not so boring not.people" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Made with 🫶🏻 by not so boring not.people" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Hecho con 🫶🏻 por gente no tan aburrida" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Fait avec 🫶🏻 par des personnes pas si ennuyeuses - boring not.people" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Made with 🫶🏻 by not so boring not.people" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Realizzato col 🫶🏻 da not so boring not.people" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "지루하지 않은 비사람들이 🫶🏻 마음을 담아 만들었습니다" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Zrobione z 🫶🏻 przez niezbyt nudnych ludzi" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Made with 🫶🏻 by not so boring not.people" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Made with 🫶🏻 by not so boring not.people" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "O kadar da sıkıcı olmayan insanlar tarafından 🫶🏻 ile yapılmıştır" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Made with 🫶🏻 by not so boring not.people" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "不那么无趣的 not.people 团队 用🫶🏻出品" } } } }, "Mark as complete" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Mark as complete" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Označit jako dokončené" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Als erledigt markieren" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Mark as complete" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Mark as complete" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Marcar como completado" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Marquer comme terminé" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Mark as complete" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Segna come completato" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "완료로 표시" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Oznacz jako ukończone" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Mark as complete" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Mark as complete" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Tamamlanmış olarak işaretle" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Позначити як виконане" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "标记为已完成" } } } }, "Mark as incomplete" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Mark as incomplete" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Označit jako nedokončené" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Als unvollständig markieren" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Mark as incomplete" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Mark as incomplete" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Marcar como incompleto" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Marquer comme incomplet" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Mark as incomplete" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Segna come incompleto" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "미완료로 표시" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Oznacz jako nieukończone" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Mark as incomplete" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Mark as incomplete" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Tamamlanmamış olarak işaretle" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Позначити як незавершене" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "标记为未完成" } } } }, "Match menu bar height" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Match menu bar height" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Match menu bar height" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "An Höhe der Menüleiste anpassen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Match menu bar height" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Match menu bar height" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Match menu bar height" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Match menu bar height" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Match menu bar height" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Match menu bar height" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "메뉴바 높이에 맞추기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Dopasuj do wysokości paska menu" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Match menu bar height" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Match menu bar height" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Match menu bar height" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Match menu bar height" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Match menu bar height" } } } }, "Match menubar height" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Match menubar height" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Odpovídat výšce menubaru" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Höhe der Menüleiste" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Match menubar height" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Match menubar height" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Igualar la altura de la Barra de menús" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Atteindre la hauteur de la barre de menu" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Match menubar height" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Adatta altezza menubar" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "메뉴바 높이에 맞추기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Dopasuj do wysokości paska menu" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Match menubar height" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Match menubar height" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Menubar yüksekliğini eşle" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Match menubar height" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "匹配菜单栏高度" } } } }, "Match real notch height" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Match real notch height" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Match real notch height" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "An Größe der echten Notch anpassen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Match real notch height" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Match real notch height" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Match real notch height" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Match real notch height" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Match real notch height" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Match real notch height" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "실제 노치 크기에 맞추기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Dopasuj do rzeczywistego rozmiaru notcha" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Match real notch height" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Match real notch height" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Match real notch height" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Match real notch height" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Match real notch height" } } } }, "Max Capacity: %lld%%" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Max Capacity: %lld%%" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Maximální kapacita: %lld%%" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Max. Kapazität: %lld%%" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Max Capacity: %lld%%" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Max Capacity: %lld%%" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Capacidad máxima: %lld%%" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Capacité maximale : %lld%%" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Max Capacity: %lld%%" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Capacità Massima: %lld%%" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "최대 용량: %lld%%" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Maksymalna pojemność: %lld%%" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Max Capacity: %lld%%" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Max Capacity: %lld%%" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Maksimum Kapasite: %lld%%" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Max Capacity: %lld%%" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "最大容量:%lld%%" } } } }, "Media" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Media" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Média" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Medium" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Media" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Media" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Multimedia" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Média" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Media" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Media" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "미디어" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Multimedia" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Media" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Media" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Medya" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Media" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "媒体" } } } }, "Media controls" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Media controls" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Ovládání médií" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Mediensteuerelemente" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Media controls" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Media controls" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Controles multimedia" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Contrôles des médias" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Media controls" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Controlli multimediali" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "미디어 컨트롤" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Ustawienia multimediów" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Media controls" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Media controls" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Medya kontrolleri" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Media controls" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "媒体控制" } } } }, "Media inactivity timeout" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Media inactivity timeout" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Časový limit nečinnosti médií" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Timeout für Medieninaktivität" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Media inactivity timeout" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Media inactivity timeout" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Tiempo de espera para inactividad multimedia" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Délai d'inactivité du média" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Media inactivity timeout" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Timeout inattività media" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "미디어 비활성화 시간 초과" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Limit czasu braku aktywności multimediów" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Media inactivity timeout" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Media inactivity timeout" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Medya etkin olmayan zaman aşımı" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Media inactivity timeout" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "媒体非活动超时" } } } }, "Media playback live activity" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Media playback live activity" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Živá aktivita přehrávání médií" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Live-Aktivität für Medien" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Media playback live activity" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Media playback live activity" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Actividad de reproducción multimedia" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Activité de lecture de média en direct" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Media playback live activity" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Riproduzione media attività live" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "미디어 재생 실시간 활동" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Odtwarzanie multimediów na żywo" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Atividade ao vivo da reprodução de mídia" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Media playback live activity" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Medya oynatma canlı etkinliği" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Media playback live activity" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "媒体播放实时动态" } } } }, "Media Source" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Media Source" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Zdroj médií" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Medienquelle" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Media Source" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Media Source" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Fuente de la multimedia" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Source du Média" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Media Source" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Sorgente multimediale" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "미디어 소스" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Źródło multimediów" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Media Source" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Media Source" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Medya kaynağı" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Media Source" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "媒体来源" } } } }, "Medium" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Medium" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Střední" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Mittel" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Medium" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Medium" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Mediano" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Moyen" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Medium" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Media" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "중간" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Średni" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Medium" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Medium" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Orta" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Medium" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "中" } } } }, "Mic %@" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Mic %@" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Mikrofon %@" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Mikrofon %@" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Mic %@" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Mic %@" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Micrófono %@" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Mic %@" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Mic %@" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Mic %@" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "마이크 %@" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Mikrofon %@" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Mic %@" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Mic %@" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Mikrofon %@" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Mic %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "麦克风 %@" } } } }, "Mirror" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Mirror" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Zrcátko" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Spiegel" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Mirror" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Mirror" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Espejo" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Miroir" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Mirror" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Specchio" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "거울" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Lustro" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Mirror" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Mirror" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Ayna" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Mirror" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "镜子" } } } }, "Mirror shape" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Mirror shape" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Tvar zrcadla" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Spiegelform" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Mirror shape" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Mirror shape" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Forma del espejo" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Forme du miroir" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Mirror shape" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Forma dello specchio" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "거울 모양" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Kształt lustra" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Mirror shape" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Mirror shape" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Ayna şekli" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Mirror shape" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "镜子大小" } } } }, "More" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "More" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Více" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Mehr" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "More" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "More" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Más" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Plus" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "More" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Altro" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "더" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Więcej" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "More" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "More" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Daha fazla" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "More" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "更多" } } } }, "Music Source" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Music Source" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Zdroj hudby" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Musikquelle" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Music Source" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Music Source" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Fuente de la música" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Source de la musique" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Music Source" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Sorgente Musica" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "음악 소스" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Źródło muzyki" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Music Source" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Music Source" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Müzik kaynağı" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Music Source" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "音乐源" } } } }, "muted" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "muted" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "ztlumeno" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Stumm" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "muted" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "muted" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "audio desactivado" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "en sourdine" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "muted" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "mutato" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "음소거됨" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Wyciszony" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "muted" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "muted" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "susturuldu" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "muted" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "已静音" } } } }, "Muted" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Muted" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Muted" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Muted" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Muted" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Muted" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Muted" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Muted" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Muted" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Muted" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "Muted" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Muted" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Muted" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Muted" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Muted" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Muted" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Muted" } } } }, "Name" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Name" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Jméno" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Name" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Name" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Name" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Nombre" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Nom" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Name" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Nome" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "이름" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Nazwa" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Name" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Name" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "İsim" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Name" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "名称" } } } }, "Never hide" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Never hide" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Nikdy neskrývat" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Niemals ausblenden" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Never hide" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Never hide" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Nunca ocultar" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Ne jamais masquer" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Never hide" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Non nascondere mai" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "숨기지 않기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Nigdy nie ukrywaj" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Never hide" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Never hide" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Asla gizleme" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Never hide" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "永不隐藏" } } } }, "No custom animation available" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "No custom animation available" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Žádná vlastní animace není k dispozici" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Keine benutzerdefinierte Animation verfügbar" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "No custom animation available" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "No custom animation available" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Sin animación personalizada" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Aucune animation personnalisée disponible" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "No custom animation available" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Nessuna animazione personalizzata disponibile" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "사용자 정의 애니메이션 없음" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Brak dostępnych niestandardowych animacji" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "No custom animation available" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "No custom animation available" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Özel animasyon mevcut değil" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "No custom animation available" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "没有可用的自定义动画" } } } }, "No custom visualizer" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "No custom visualizer" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Žádný vlastní vizualizér není k dispozici" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Keine benutzerdefinierte Audiovisualisierung" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "No custom visualizer" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "No custom visualiser" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Sin visualizador personalizado" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Aucun visualiseur personnalisé" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "No custom visualizer" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Nessun visualizzatore personalizzato" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "사용자 지정 시각화 도구 없음" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Brak niestandardowych wizualizerów" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "No custom visualizer" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "No custom visualizer" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Özel görselleştirici bulunmuyor" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "No custom visualizer" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "没有自定义可视化器" } } } }, "No events" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "No events" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "No events" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Keine Ereignisse" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "No events" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "No events" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "No events" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "No events" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "No events" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "No events" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "이벤트 없음" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Brak wydarzeń" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "No events" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "No events" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "No events" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "No events" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "No events" } } } }, "No events today" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "No events today" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Žádné události" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Keine Ereignisse heute" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "No events today" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "No events today" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Sin eventos hoy" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Aucun événement aujourd'hui" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "No events today" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Non ci sono eventi" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "오늘 일정 없음" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Brak wydarzeń na dzisiaj" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "No events today" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "No events today" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Bugün başka etkinlik yok" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "No events today" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "今天无日程" } } } }, "No extension installed" : { "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "No extension installed" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Žádné rozšíření není nainstalováno" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Keine Erweiterungen installiert" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "No extension installed" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "No extension installed" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Ninguna extensión instalada" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Aucune extension installée" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "No extension installed" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Nessuna estensione installata" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "설치된 확장기능 없음" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Brak zainstalowanych rozszerzeń" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "No extension installed" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "No extension installed" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Hiçbir uzantı yüklenmemiş" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "No extension installed" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "未安装扩展" } } } }, "Not Now" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Not Now" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Teď ne" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Nicht jetzt" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Not Now" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Not Now" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Ahora no" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Pas pour l'instant" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Not Now" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Non Ora" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "나중에" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Nie teraz" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Not Now" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Not Now" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Şimdi Değil" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Not Now" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "不是现在" } } } }, "Notch behavior" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch behavior" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Chování výřezu" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Verhalten der Notch" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Notch behavior" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Notch behavior" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Comportamiento del notch" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Comportement du Notch" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch behavior" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Comportamento notch" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "노치 동작" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Zachowanie notcha" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch behavior" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch behavior" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Çentik davranışı" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch behavior" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch行为" } } } }, "Notch height on non-notch displays" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch height on non-notch displays" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch height on non-notch displays" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch Höhe auf nicht-notch Displays" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Notch height on non-notch displays" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch height on non-notch displays" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch height on non-notch displays" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch height on non-notch displays" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch height on non-notch displays" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch height on non-notch displays" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "노치 없는 디스플레이에서 노치 높이" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Wysokość notcha na wyświetlaczach bez notcha" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch height on non-notch displays" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch height on non-notch displays" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch height on non-notch displays" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch height on non-notch displays" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch height on non-notch displays" } } } }, "Notch height on notch displays" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch height on notch displays" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch height on notch displays" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch Höhe auf Notch Displays" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Notch height on notch displays" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch height on notch displays" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch height on notch displays" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch height on notch displays" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch height on notch displays" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch height on notch displays" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "노치 디스플레이에서 노치 높이" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Wysokość notcha na wyświetlaczach z notchem" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch height on notch displays" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch height on notch displays" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch height on notch displays" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch height on notch displays" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch height on notch displays" } } } }, "Notch sizing" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch sizing" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch sizing" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Notchgröße" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Notch sizing" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch sizing" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch sizing" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch sizing" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch sizing" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch sizing" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "노치 크기 조정" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Rozmiar Notcha" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch sizing" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch sizing" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch sizing" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch sizing" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch sizing" } } } }, "Open Calendar Settings" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Open Calendar Settings" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Otevřít nastavení Kalendáře" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Kalendereinstellungen öffnen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Open Calendar Settings" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Open Calendar Settings" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Abrir ajustes de Calendario" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Ouvrir les paramètres du calendrier" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Open Calendar Settings" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Apri Impostazioni Calendario" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "캘린더 설정 열기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Otwórz ustawienia kalendarza" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Open Calendar Settings" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Open Calendar Settings" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Takvim Ayarlarını Aç" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Open Calendar Settings" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "打开日历设置" } } } }, "Open Notch" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Open Notch" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Open Notch" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Open Notch" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Open Notch" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Open Notch" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Open Notch" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Open Notch" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Open Notch" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Open Notch" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "Open Notch" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Open Notch" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Open Notch" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Open Notch" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Open Notch" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Open Notch" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Open Notch" } } } }, "Open notch on hover" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Open notch on hover" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Open notch on hover" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch bei Mausberührung öffnen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Open notch on hover" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Open notch on hover" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Abrir el notch al pasar el cursor" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Ouvrir l'encoche au survol" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Open notch on hover" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Apri notch portando il mouse sopra di esso" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "갖다대서 노치 열기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Otwórz notch po najechaniu kursorem" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Open notch on hover" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Open notch on hover" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Fare sürüklendiğinde çentiği aç" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Open notch on hover" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "悬停时打开Notch" } } } }, "Open Reminder Settings" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Open Reminder Settings" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Otevřít nastavení Připomínek" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Erinnerungseinstellungen öffnen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Open Reminder Settings" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Open Reminder Settings" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Abrir ajustes de Recordatorios" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Ouvrir les paramètres de rappel" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Open Reminder Settings" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Apri Impostazioni Promemoria" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "미리알림 설정 열기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Otwórz ustawienia przypomnień" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Open Reminder Settings" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Open Reminder Settings" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Anımsatıcı Ayarlarını Aç" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Open Reminder Settings" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "打开提醒事项设置" } } } }, "Open shelf by default if items are present" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Open shelf by default if items are present" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Open shelf by default if items are present" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Standardmäßig Ablage öffnen, wenn Dateien abgelegt sind" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Open shelf by default if items are present" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Open shelf by default if items are present" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Abrir siempre la bandeja si hay elementos dentro" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Ouvrir la bibliothèque par défaut si des éléments sont présents" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Open shelf by default if items are present" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Apri automaticamente lo scaffale se contiene elementi" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "항목이 있으면 기본으로 선반 열기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Otwórz półkę domyślnie, jeśli pliki są obecne" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Open shelf by default if items are present" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Open shelf by default if items are present" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Öğeler varsa varsayılan olarak rafı aç" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Open shelf by default if items are present" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "当项目存在时默认打开暂存器" } } } }, "Option key behaviour" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Option key behaviour" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Option key behaviour" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Verhalten der Optionstaste" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Option key behaviour" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Option key behaviour" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Option key behaviour" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Option key behaviour" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Option key behaviour" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Option key behaviour" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "옵션 키 동작" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Zachowanie klawisza opcji" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Option key behaviour" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Option key behaviour" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Option key behaviour" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Option key behaviour" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Option key behaviour" } } } }, "Pick a Color" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Pick a Color" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Pick a Color" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Wähle eine Farbe" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Pick a Color" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Pick a Colour" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Pick a Color" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Pick a Color" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Pick a Color" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Pick a Color" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "색상 선택" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Wybierz kolor" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Pick a Color" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Pick a Color" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Pick a Color" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Pick a Color" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Pick a Color" } } } }, "Plugged In" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Plugged In" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Připojeno" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Angeschlossen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Plugged In" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Plugged In" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Conectado" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Branché" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Plugged In" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Collegato" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "충전 중" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Podłączony" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Plugged In" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Plugged In" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Prize takılı" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Plugged In" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "已插入" } } } }, "Preferred display" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Preferred display" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Preferred display" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Bevorzugter Bildschirm" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Preferred display" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Preferred display" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Preferred display" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Preferred display" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Preferred display" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Preferred display" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "선호 디스플레이" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Preferowany wyświetlacz" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Preferred display" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Preferred display" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Preferred display" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Preferred display" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Preferred display" } } } }, "Progress bar style" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Progress bar style" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Progress bar style" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Progress bar style" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Progress bar style" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Progress bar style" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Progress bar style" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Progress bar style" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Progress bar style" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Progress bar style" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "Progress bar style" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Progress bar style" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Progress bar style" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Progress bar style" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Progress bar style" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Progress bar style" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Progress bar style" } } } }, "Quick Share" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Quick Share" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Quick Share" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Schnellteilen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Quick Share" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Quick Share" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Quick Share" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Quick Share" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Quick Share" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Quick Share" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "퀵 쉐어" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Szybkie udostępnianie" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Quick Share" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Quick Share" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Quick Share" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Quick Share" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Quick Share" } } } }, "Quick Share Service" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Quick Share Service" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Quick Share Service" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Schnellteildienst" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Quick Share Service" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Quick Share Service" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Quick Share Service" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Quick Share Service" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Quick Share Service" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Quick Share Service" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "퀵 쉐어 서비스" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Usługa szybkiego udostępniania" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Quick Share Service" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Quick Share Service" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Quick Share Service" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Quick Share Service" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Quick Share Service" } } } }, "Quit" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Quit" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Ukončit" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Verlassen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Quit" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Quit" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Cerrar" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Quitter" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Quit" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Chiudi" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "종료" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Wyjdź" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Quit" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Quit" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Çık" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Quit" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "退出" } } } }, "Quit app" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Quit app" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Ukončit aplikaci" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "App verlassen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Quit app" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Quit app" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Cerrar aplicación" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Quitter l'application" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Quit app" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Chiudi app" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "앱 종료" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Wyjdź z aplikacji" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Quit app" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Quit app" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Uygulamadan Çık" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Quit app" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "退出应用" } } } }, "Release name" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Release name" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Název vydání" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Veröffentlichungsname" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Release name" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Release name" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Nombre de la versión" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Nom de la version" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Release name" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Nome del rilascio" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "릴리즈 이름" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Nazwa wydania" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Release name" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Release name" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Yayın İsmi" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Release name" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "版本名称" } } } }, "Remember last tab" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Remember last tab" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Zapamatovat poslední kartu" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Merke den letzten Tab" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Remember last tab" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Remember last tab" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Recordar la última pestaña" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Se souvenir du dernier onglet" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Remember last tab" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Ricorda l'ultima scheda" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "마지막 탭 기억하기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Zapamiętaj ostatnią kartę" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Remember last tab" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Remember last tab" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Son sekmeyi hatırla" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Remember last tab" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "记住上一个标签页" } } } }, "Reminder access is denied. Please enable it in System Settings." : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Reminder access is denied. Please enable it in System Settings." } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Přístup do kalendáře byl odepřen. Prosím povolte jej v nastavení systému." } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Erinnerungen Zugriff verweigert. Bitte aktiviere es in den Systemeinstellungen." } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Reminder access is denied. Please enable it in System Settings." } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Reminder access is denied. Please enable it in System Settings." } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Acceso a Recordatorios denegado. Conceda acceso en Configuración del Sistema" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "L'accès au rappel est refusé. Veuillez l'activer dans les paramètres." } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Reminder access is denied. Please enable it in System Settings." } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "L'accesso al promemoria è negato. Si prega di abilitarlo nelle impostazioni di sistema." } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "미리알림 엑세스 거부됨. 시스템설정에서 허용해주세요." } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Odmowa dostępu do przypomnień. Proszę włączyć dostęp w ustawieniach systemowych." } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Reminder access is denied. Please enable it in System Settings." } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Reminder access is denied. Please enable it in System Settings." } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Hatırlatıcılar erişimi reddedildi. Lütfen Sistem Ayarlarından izin verin." } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Reminder access is denied. Please enable it in System Settings." } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "提醒事项访问被拒绝。请在系统设置中启用它。" } } } }, "Reminders" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Reminders" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Připomínky" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Erinnerungen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Reminders" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Reminders" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Recordatorios" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Rappels" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Reminders" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Promemoria" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "미리알림들" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Przypomnienia" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Reminders" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Reminders" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Hatırlatıcılar" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Reminders" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "提醒事项" } } } }, "Remove from shelf after dragging" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Remove from shelf after dragging" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Remove from shelf after dragging" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Nach Ziehen aus der Ablage entfernen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Remove from shelf after dragging" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Remove from shelf after dragging" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Remove from shelf after dragging" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Remove from shelf after dragging" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Remove from shelf after dragging" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Remove from shelf after dragging" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "드래그 후 선반에서 제거" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Usuń z półki po przeciąganiu" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Remove from shelf after dragging" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Remove from shelf after dragging" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Remove from shelf after dragging" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Remove from shelf after dragging" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Remove from shelf after dragging" } } } }, "Replace system HUD" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Replace system HUD" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Replace system HUD" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "System HUD ersetzen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Replace system HUD" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Replace system HUD" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Replace system HUD" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Replace system HUD" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Replace system HUD" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Replace system HUD" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "시스템 HUD 교체" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Zastąp system HUD" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Replace system HUD" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Replace system HUD" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Replace system HUD" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Replace system HUD" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Replace system HUD" } } } }, "Replaces the standard macOS volume, display brightness, and keyboard brightness HUDs with a custom design." : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Replaces the standard macOS volume, display brightness, and keyboard brightness HUDs with a custom design." } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Replaces the standard macOS volume, display brightness, and keyboard brightness HUDs with a custom design." } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Replaces the standard macOS volume, display brightness, and keyboard brightness HUDs with a custom design." } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Replaces the standard macOS volume, display brightness, and keyboard brightness HUDs with a custom design." } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Replaces the standard macOS volume, display brightness, and keyboard brightness HUDs with a custom design." } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Replaces the standard macOS volume, display brightness, and keyboard brightness HUDs with a custom design." } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Replaces the standard macOS volume, display brightness, and keyboard brightness HUDs with a custom design." } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Replaces the standard macOS volume, display brightness, and keyboard brightness HUDs with a custom design." } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Replaces the standard macOS volume, display brightness, and keyboard brightness HUDs with a custom design." } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "Replaces the standard macOS volume, display brightness, and keyboard brightness HUDs with a custom design." } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Replaces the standard macOS volume, display brightness, and keyboard brightness HUDs with a custom design." } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Replaces the standard macOS volume, display brightness, and keyboard brightness HUDs with a custom design." } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Replaces the standard macOS volume, display brightness, and keyboard brightness HUDs with a custom design." } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Replaces the standard macOS volume, display brightness, and keyboard brightness HUDs with a custom design." } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Replaces the standard macOS volume, display brightness, and keyboard brightness HUDs with a custom design." } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Replaces the standard macOS volume, display brightness, and keyboard brightness HUDs with a custom design." } } } }, "Request Accessibility" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Request Accessibility" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Request Accessibility" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Bedienungshilfen anfordern" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Request Accessibility" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Request Accessibility" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Request Accessibility" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Request Accessibility" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Request Accessibility" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Request Accessibility" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "접근성 권한 요청" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Prośba o dostępność" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Request Accessibility" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Request Accessibility" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Request Accessibility" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Request Accessibility" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Request Accessibility" } } } }, "Reset to Defaults" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Reset to Defaults" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Reset to Defaults" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Auf Standard zurücksetzen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Reset to Defaults" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Reset to Defaults" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Reset to Defaults" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Reset to Defaults" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Reset to Defaults" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Reset to Defaults" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "기본값으로 재설정" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Przywróć ustawienia domyślne" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Reset to Defaults" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Reset to Defaults" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Reset to Defaults" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Reset to Defaults" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Reset to Defaults" } } } }, "Restart" : { "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Restart" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Restartovat" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Neustarten" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Restart" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Restart" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Reiniciar" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Redémarrer" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Restart" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Riavvia" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "재시작" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Uruchom ponownie" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Restart" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Restart" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Yeniden başlat" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Restart" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "重启" } } } }, "Restart Boring Notch" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Restart Boring Notch" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Restartovat Boring Notch" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Boring Notch neu starten" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Restart Boring Notch" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Restart Boring Notch" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Reiniciar Boring Notch" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Redémarrer Boring Notch" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Restart Boring Notch" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Riavvia Boring Notch" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "Boring Notch 재시작" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Uruchom ponownie Boring Notch" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Restart Boring Notch" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Restart Boring Notch" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Boring Notch'u yeniden başlat" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Restart Boring Notch" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "重启 Boring Notch" } } } }, "Running" : { "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Running" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Běží" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Läuft" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Running" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Running" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Ejecutándose" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "En cours d'exécution" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Running" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "In esecuzione" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "실행 중" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Uruchomiony" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Running" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Running" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Çalışıyor" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Running" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "运行中" } } } }, "Select the music source you want to use. You can change this later in the app settings." : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Select the music source you want to use. You can change this later in the app settings." } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Vyberte zdroj hudby který chcete používat. Můžete jej později změnit v nastavení aplikace." } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Wähle die Musik Quelle aus, die du benutzen willst. Du kannst dies später in den Einstellungen ändern." } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Select the music source you want to use. You can change this later in the app settings." } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Select the music source you want to use. You can change this later in the app settings." } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Seleccione la fuente de la música que desea utilizar. Puede cambiar esto más tarde en los ajustes de la aplicación." } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Sélectionnez la source de musique que vous souhaitez utiliser. Vous pourrez la modifier plus tard dans les paramètres de l'application." } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Select the music source you want to use. You can change this later in the app settings." } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Selezionare la sorgente musicale che si desidera utilizzare. È possibile modificare questa impostazione in seguito nelle impostazioni dell'app." } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "음악 소스 선택하세요. 나중에 앱 설정에서 변경 가능합니다." } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Wybierz źródło muzyki, którego chcesz użyć. Możesz to później zmienić w ustawieniach aplikacji." } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Select the music source you want to use. You can change this later in the app settings." } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Select the music source you want to use. You can change this later in the app settings." } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Kullanmak istediğiniz müzik kaynağını seçin. Bunu daha sonra uygulama ayarlarından değiştirebilirsiniz." } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Select the music source you want to use. You can change this later in the app settings." } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "选择您想要使用的音乐源。您可以稍后在应用设置中更改此项。" } } } }, "selected" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "selected" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "vybráno" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "ausgewählt" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "selected" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "selected" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "seleccionado(s)" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "sélectionné(s)" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "selected" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "selezionato" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "선택된" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "zaznaczone" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "selected" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "selected" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "seçildi" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "selected" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "已选择" } } } }, "Selected animation" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Selected animation" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Vybraná animace" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Ausgewählte Animation" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Selected animation" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Selected animation" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Animación seleccionada" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Animations sélectionné(es)" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Selected animation" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Animazione selezionata" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "선택된 애니메이션" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Wybrana animacja" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Selected animation" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Selected animation" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Seçili animasyon" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Selected animation" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "所选动画" } } } }, "Settings" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Settings" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Nastavení" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Einstellungen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Settings" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Settings" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Ajustes" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Paramètres" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Settings" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Impostazioni" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "설정" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Ustawienia" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Settings" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Settings" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Ayarlar" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Settings" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "设置" } } } }, "Shelf" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Shelf" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Police" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Ablage" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Shelf" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Shelf" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Bandeja" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Étagère" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Shelf" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Scaffale" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "서랍" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Półka" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Shelf" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Shelf" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Raf" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Shelf" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "暂存器" } } } }, "Shortcuts" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Shortcuts" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Zkratky" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Tastaturkürzel" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Shortcuts" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Shortcuts" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Atajos" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Raccourcis" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Shortcuts" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Scorciatoie" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "바로가기들" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Skróty klawiszowe" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Shortcuts" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Shortcuts" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Kestirmeler" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Shortcuts" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "快捷方式" } } } }, "Show battery indicator" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Show battery indicator" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Zobrazit stav baterie" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Batterie-Symbol anzeigen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Show battery indicator" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Show battery indicator" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Mostrar indicador de la batería" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Afficher l'indicateur de la batterie" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Show battery indicator" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Mostra indicatore della batteria" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "배터리 상태 표시하기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Pokaż wskaźnik baterii" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Show battery indicator" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Show battery indicator" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Pil göstergesini göster" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Show battery indicator" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "显示电量" } } } }, "Show battery percentage" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Show battery percentage" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Zobrazit % baterie" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Batterieanzeige in Prozent" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Show battery percentage" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Show battery percentage" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Mostrar porcentaje de la batería" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Afficher le pourcentage de la batterie" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Show battery percentage" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Mostra percentuale batteria" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "배터리 퍼센트 표시하기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Pokaż procent baterii" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Show battery percentage" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Show battery percentage" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Pil yüzdesini göster" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Show battery percentage" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "显示电量百分比" } } } }, "Show calendar" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Show calendar" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Zobrazit kalendář" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Kalender anzeigen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Show calendar" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Show calendar" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Mostrar Calendario" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Afficher le calendrier" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Show calendar" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Mostra calendario" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "캘린더 보이기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Pokaż kalendarz" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Show calendar" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Show calendar" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Takvimi göster" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Show calendar" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "显示日历" } } } }, "Show cool face animation while inactive" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Show cool face animation while inactive" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Show cool face animation while inactive" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Zeige coole Gesichtsanimation während Inaktivität" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Show cool face animation while inactive" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Show cool face animation while inactive" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Show cool face animation while inactive" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Show cool face animation while inactive" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Show cool face animation while inactive" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Show cool face animation while inactive" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "비활성 시 쿨한 얼굴 애니메이션 표시" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Wyświetlaj fajną animację twarzy podczas bezczynności" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Show cool face animation while inactive" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Show cool face animation while inactive" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Show cool face animation while inactive" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Show cool face animation while inactive" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Show cool face animation while inactive" } } } }, "Show HUD in open notch" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Show HUD in open notch" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Show HUD in open notch" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Show HUD in open notch" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Show HUD in open notch" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Show HUD in open notch" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Show HUD in open notch" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Show HUD in open notch" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Show HUD in open notch" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Show HUD in open notch" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "Show HUD in open notch" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Show HUD in open notch" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Show HUD in open notch" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Show HUD in open notch" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Show HUD in open notch" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Show HUD in open notch" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Show HUD in open notch" } } } }, "Show lyrics below artist name" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Show lyrics below artist name" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Show lyrics below artist name" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Songtexte unter Künstlernamen anzeigen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Show lyrics below artist name" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Show lyrics below artist name" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Show lyrics below artist name" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Show lyrics below artist name" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Show lyrics below artist name" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Show lyrics below artist name" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "아티스트 이름 아래 가사 표시" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Pokaż tekst utworu pod nazwą wykonawcy" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Show lyrics below artist name" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Show lyrics below artist name" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Show lyrics below artist name" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Show lyrics below artist name" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Show lyrics below artist name" } } } }, "Show menu bar icon" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Show menu bar icon" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Show menu bar icon" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Menüleistensymbol anzeigen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Show menu bar icon" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Show menu bar icon" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Show menu bar icon" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Show menu bar icon" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Show menu bar icon" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Show menu bar icon" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "메뉴 막대 아이콘 표시" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Włącz ikonę na pasku menu" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Show menu bar icon" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Show menu bar icon" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Show menu bar icon" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Show menu bar icon" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Show menu bar icon" } } } }, "Show music live activity" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Show music live activity" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Show music live activity" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Musik-Live-Aktivität anzeigen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Show music live activity" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Show music live activity" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Show music live activity" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Show music live activity" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Show music live activity" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Show music live activity" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "음악 실시간 현황 켜기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Pokaż aktywność muzyczną na żywo" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Show music live activity" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Show music live activity" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Show music live activity" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Show music live activity" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Show music live activity" } } } }, "Show notch on lock screen" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Show notch on lock screen" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Show notch on lock screen" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch auf dem Sperrbildschirm anzeigen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Show notch on lock screen" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Show notch on lock screen" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Show notch on lock screen" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Show notch on lock screen" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Show notch on lock screen" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Show notch on lock screen" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "잠금 화면에 노치 표시" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Pokaż notch na ekranie blokady" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Show notch on lock screen" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Show notch on lock screen" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Show notch on lock screen" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Show notch on lock screen" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Show notch on lock screen" } } } }, "Show on all displays" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Show on all displays" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Zobrazit na všech obrazovkách" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Auf allen Displays anzeigen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Show on all displays" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Show on all displays" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Mostrar en todas las pantallas" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Afficher sur tous les écrans" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Show on all displays" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Mostra su tutti gli schermi" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "모든 디스플레이에 표시" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Pokaż na wszystkich wyświetlaczach" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Show on all displays" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Show on all displays" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Tüm ekranlarda göster" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Show on all displays" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "在所有显示器上显示" } } } }, "Show percentage" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Show percentage" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Show percentage" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Show percentage" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Show percentage" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Show percentage" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Show percentage" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Show percentage" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Show percentage" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Show percentage" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "Show percentage" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Show percentage" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Show percentage" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Show percentage" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Show percentage" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Show percentage" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Show percentage" } } } }, "Show power status icons" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Show power status icons" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Zobrazit ikonu stavu napájení" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Symbole zum Ladestatus anzeigen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Show power status icons" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Show power status icons" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Mostrar iconos del adaptador de corriente" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Afficher les icônes d'états de chargement" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Show power status icons" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Mostra icona stato alimentazione" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "전원 상태 아이콘 보이기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Pokaż ikony stanu zasilania" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Show power status icons" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Show power status icons" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Güç durumu simgelerini göster" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Show power status icons" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "显示电源状态图标" } } } }, "Show power status notifications" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Show power status notifications" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Zobrazovat oznámení o stavu napájení" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Benachrichtigungen zum Ladestatus anzeigen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Show power status notifications" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Show power status notifications" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Mostrar notificaciones del adaptador de corriente" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Afficher les notifications d'état de chargement" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Show power status notifications" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Mostra notifiche alimentazione" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "전원 상태 알림 켜기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Pokaż powiadomienia o stanie zasilania" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Show power status notifications" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Show power status notifications" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Güç durumu bildirimlerini göster" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Show power status notifications" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "显示电源状态通知" } } } }, "Show settings icon in notch" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Show settings icon in notch" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Show settings icon in notch" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Einstellungssymbol in Noch anzeigen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Show settings icon in notch" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Show settings icon in notch" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Show settings icon in notch" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Show settings icon in notch" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Show settings icon in notch" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Show settings icon in notch" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "노치에 설정 아이콘 보이기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Pokaż ikonę ustawień w notchu" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Show settings icon in notch" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Show settings icon in notch" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Show settings icon in notch" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Show settings icon in notch" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Show settings icon in notch" } } } }, "Show sneak peek on playback changes" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Show sneak peek on playback changes" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Show sneak peek on playback changes" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Zeige Sneakpeek bei Wiedergabeänderungen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Show sneak peek on playback changes" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Show sneak peek on playback changes" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Show sneak peek on playback changes" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Show sneak peek on playback changes" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Show sneak peek on playback changes" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Show sneak peek on playback changes" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "재생 변경 시 훔쳐보기 표시" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Pokaż podgląd przy zmianach odtwarzania" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Show sneak peek on playback changes" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Show sneak peek on playback changes" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Show sneak peek on playback changes" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Show sneak peek on playback changes" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Show sneak peek on playback changes" } } } }, "Slider color" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Slider color" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Barva posuvníku" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Schieberegler-Farbe" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Slider color" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Slider colour" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Color de la barra de progreso" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Couleur de la barre de défilement" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Slider color" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Colore cursore" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "슬라이더 컬러" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Kolor suwaka" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Slider color" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Slider color" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Kaydırıcı rengi" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Slider color" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "滑块颜色" } } } }, "Sneak Peek shows the media title and artist under the notch for a few seconds." : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Sneak Peek shows the media title and artist under the notch for a few seconds." } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Náhled zobrazuje název skladby a autora pod výřezem na několik vteřin." } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Sneak Peek zeigt für ein paar Sekunden den Medientitel und den Künstler unter der Notch an." } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Sneak Peek shows the media title and artist under the notch for a few seconds." } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Sneak Peek shows the media title and artist under the notch for a few seconds." } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Vistazo Rápido muestra el título y nombre del artista debajo del notch por unos segundos." } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Sneak Peek montre le titre du média et l'artiste sous l'encoche pendant quelques secondes." } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Sneak Peek shows the media title and artist under the notch for a few seconds." } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Sneak Peek mostra per alcuni secondi il titolo dei media e l'artista sotto la notch." } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "훔쳐보기가 몇 초 동안 미디어 제목과 아티스트명을 노치 아래에 보여줍니다." } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Podgląd pokazuje tytuł multimediów i artystę pod notchem przez kilka sekund." } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Sneak Peek shows the media title and artist under the notch for a few seconds." } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Sneak Peek shows the media title and artist under the notch for a few seconds." } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Ön izleme, birkaç saniye boyunca çentik altında medya başlığını ve sanatçıyı gösterir." } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Sneak Peek shows the media title and artist under the notch for a few seconds." } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Sneak Peek 在 Notch 下显示媒体标题和艺术家几秒钟。" } } } }, "Sneak Peek Style" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Sneak Peek Style" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Styl náhledu" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Sneak Peek Stil" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Sneak Peek Style" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Sneak Peek Style" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Estilo del Vistazo Rápido" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Style du Sneak Peek" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Sneak Peek Style" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Stile Sneak Peek" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "훔쳐보기 스타일" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Styl podglądu" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Sneak Peek Style" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Sneak Peek Style" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Ön izleme stili" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Sneak Peek Style" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Sneak Peek 样式" } } } }, "Software updates" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Software updates" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Aktualizace" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Softwareupdates" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Software updates" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Software updates" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Actualizaciones de software" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Mises à jour du logiciel" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Software updates" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Aggiornamenti software" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "소프트웨어 업데이트" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Aktualizacje oprogramowania" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Software updates" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Software updates" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Yazılım güncellemeleri" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Software updates" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "软件更新" } } } }, "Speed" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Speed" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Rychlost" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Geschwindigkeit" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Speed" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Speed" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Velocidad" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Vitesse" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Speed" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Velocità" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "속도" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Szybkość" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Speed" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Speed" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Hız" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Speed" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "速度" } } } }, "Square" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Square" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Čtverec" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Quadratisch" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Square" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Square" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Cuadrado" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Carré" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Square" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Quadrato" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "사각형" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Kwadrat" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Square" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Square" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Kare" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Square" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "方形" } } } }, "Stopped" : { "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Stopped" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Zastaveno" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Gestoppt" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Stopped" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Stopped" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Detenido" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Arrêt" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Stopped" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Interrotto" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "중지 됨" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Zatrzymano" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Stopped" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Stopped" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Durduruldu" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Stopped" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "已停止" } } } }, "Support Us" : { "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Support Us" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Podpořte nás" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Unterstütze uns" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Support Us" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Support Us" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Apóyenos" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Soutenez-nous" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Support Us" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Supportaci" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "후원하기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Wesprzyj nas" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Support Us" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Support Us" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Bizi Destekle" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Support Us" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "支持我们" } } } }, "System" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "System" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "System" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "System" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "System" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "System" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "System" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "System" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "System" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "System" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "시스템" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "System" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "System" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "System" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "System" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "System" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "System" } } } }, "System features" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "System features" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Systémové funkce" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Systemfunktionen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "System features" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "System features" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Características del Sistema" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Fonctionnalités système" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "System features" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Funzionalità di sistema" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "시스템 기능" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Funkcje systemowe" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "System features" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "System features" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Sistem özellikleri" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "System features" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "系统功能" } } } }, "Time to Full Charge: %lld min" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Time to Full Charge: %lld min" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Čas do plného nabití: %lld min" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Zeit bis zur vollen Ladung: %lld min" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Time to Full Charge: %lld min" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Time to Full Charge: %lld min" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Tiempo para recarga completa: %lld min" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Temps avant la charge complète : %lld min" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Time to Full Charge: %lld min" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Tempo alla carica completa: %lld min" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "완충 까지 남은 시간: %lld 분" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Czas do pełnego naładowania: %lld min" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Time to Full Charge: %lld min" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Time to Full Charge: %lld min" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Tam Şarj Süresi: %lld dakika" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Time to Full Charge: %lld min" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "充电时间:%lld 分钟" } } } }, "Tint progress bar with accent color" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Tint progress bar with accent color" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Tint progress bar with accent color" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Leiste in Akzentfarbe färben" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Tint progress bar with accent color" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Tint progress bar with accent colour" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Tint progress bar with accent color" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Tint progress bar with accent color" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Tint progress bar with accent color" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Tint progress bar with accent color" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "강조 색상으로 진행 바 색상 변경" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Przyciemnij pasek postępu przy użyciu koloru akcentu" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Tint progress bar with accent color" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Tint progress bar with accent color" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Tint progress bar with accent color" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Tint progress bar with accent color" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Tint progress bar with accent color" } } } }, "Toggle Notch Open:" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Toggle Notch Open:" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Zapnout otevření výřezu:" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Notch öffnen umschalten:" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Toggle Notch Open:" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Toggle Notch Open:" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Habilitar apertura del notch:" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Activer/désactiver le Notch Ouvert:" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Toggle Notch Open:" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Apri Notch:" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "노치 열기 토글:" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Włącz/wyłącz Notch:" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Toggle Notch Open:" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Toggle Notch Open:" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Çentik Aç/Kapat:" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Toggle Notch Open:" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "切换Notch:" } } } }, "Toggle Sneak Peek:" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Toggle Sneak Peek:" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Zapnout náhled:" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Sneak Peek umschalten:" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Toggle Sneak Peek:" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Toggle Sneak Peek:" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Habilitar Vistazo Rápido:" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Activer/désactiver Sneak Peek:" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Toggle Sneak Peek:" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Apri Sneak Peek:" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "훔쳐보기 토글:" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Włącz/wyłącz podgląd:" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Toggle Sneak Peek:" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Toggle Sneak Peek:" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Ön izlemeyi Aç/Kapat:" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Toggle Sneak Peek:" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "触发 Sneak Peek:" } } } }, "Two-finger swipe up on notch to close, two-finger swipe down on notch to open when **Open notch on hover** option is disabled" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Two-finger swipe up on notch to close, two-finger swipe down on notch to open when **Open notch on hover** option is disabled" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Posunutím dvěma prsty nahoru zavřete, dvojitým posunem dolů otevřete, když je zakázána možnost **Otevření výřezu při nápovědě**" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Zwei-Finger Wischen nach oben zum Schließen der Notch, zwei-Finger wischen nach unten um die Notch zu öffnen, wenn die Option **Notch bei Mausberührung öffnen** deaktiviert ist" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Two-finger swipe up on notch to close, two-finger swipe down on notch to open when **Open notch on hover** option is disabled" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Two-finger swipe up on notch to close, two-finger swipe down on notch to open when **Open notch on hover** option is disabled" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Deslice con dos dedos hacia arriba para cerrar, deslice con dos dedos hacia abajo para abrir cuando la opción de **Abrir el notch al pasar el cursor** esté deshabilitada" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Glisser deux doigts vers le haut sur l'encoche pour fermer, glisser deux doigts vers le bas sur l'encoche pour l'ouvrir lorsque l'option **Ouvrir l'encoche au survolant** est désactivée" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Two-finger swipe up on notch to close, two-finger swipe down on notch to open when **Open notch on hover** option is disabled" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Scorri due dita verso l'alto sulla notch per chiudere, scorri due dita verso il basso sulla tacca per aprirla quando l'opzione **Apri notch all'hover ** è disabilitata" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "노치에 가져다 대서 열기 옵션이 꺼져있을 때, 노치를 두 손가락을 위로 밀어서 닫고, 두 손가락을 아래로 내려서 열기" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Przesuń dwoma palcami w górę na notchu, aby zamknąć, przesuń dwoma palcami w dół na notchu, aby otworzyć, gdy opcja **Otwórz notch przy najechaniu palcem ** jest wyłączona" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Two-finger swipe up on notch to close, two-finger swipe down on notch to open when **Open notch on hover** option is disabled" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Two-finger swipe up on notch to close, two-finger swipe down on notch to open when **Open notch on hover** option is disabled" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "**Fareyi üzerine getirdiğinde aç** seçeneği devre dışı bırakıldığında, kapatmak için çentiği iki parmağınızla yukarı kaydırın, açmak için çentiği iki parmağınızla aşağı kaydırın." } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Two-finger swipe up on notch to close, two-finger swipe down on notch to open when **Open notch on hover** option is disabled" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "在Notch里双指向上滑动以关闭。当**悬停在Notch时打开**选项禁用时,双指向下滑动即可打开" } } } }, "Uninstall" : { "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Uninstall" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Odinstalovat" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Deinstallieren" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Uninstall" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Uninstall" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Desinstalar" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Désinstaller" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Uninstall" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Disinstalla" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "설치제거" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Odinstaluj" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Uninstall" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Uninstall" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Yüklemeyi Kaldır" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Uninstall" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "卸载" } } } }, "Unlock advanced features and improve your experience. Upgrade now for more customizations!" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Unlock advanced features and improve your experience. Upgrade now for more customizations!" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Odemkněte pokročilé funkce a vylepšete si svůj zážitek. Upgradujte nyní pro více přizpůsobení!" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Schalte erweiterte Funktionen frei und verbessere deine Erfahrung. Upgrade jetzt für weitere Anpassungsmöglichkeiten!" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Unlock advanced features and improve your experience. Upgrade now for more customizations!" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Unlock advanced features and improve your experience. Upgrade now for more customizations!" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Desbloquee funciones avanzadas y mejore su experiencia. ¡Actualice ahora para más personalizaciones!" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Déverrouillez des fonctionnalités avancées et améliorez votre expérience. Mettez à niveau maintenant pour plus de personnalisations !" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Unlock advanced features and improve your experience. Upgrade now for more customizations!" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Sblocca le funzionalità avanzate e migliora la tua esperienza. Aggiorna ora per ulteriori personalizzazioni!" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "고급 설정을 해제해 경험을 향상하세요. 더 많은 맞춤화 설정을 지금 바로 업그레이드해서 즐기세요!" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Odblokuj dodatkowe funkcje i ciesz się lepszymi możliwościami. Ulepsz teraz, aby móc bardziej dostosować ustawienia!" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Unlock advanced features and improve your experience. Upgrade now for more customizations!" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Unlock advanced features and improve your experience. Upgrade now for more customizations!" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Gelişmiş özelliklerin kilidini açın ve deneyiminizi iyileştirin. Daha fazla özelleştirme için şimdi yükseltin!" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Unlock advanced features and improve your experience. Upgrade now for more customizations!" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "解锁高级功能并提升您的体验。现在升级以获取更多自定义功能!" } } } }, "unmuted" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "unmuted" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "neztlumeno" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Nicht stummgeschaltet" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "unmuted" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "unmuted" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "audio reactivado" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "sans sourdine" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "unmuted" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "smutato" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "음소거 해제됨" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Wyciszenie wyłączone" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "unmuted" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "unmuted" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "sesi açılmış" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "unmuted" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "已取消静音" } } } }, "Unmuted" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Unmuted" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Unmuted" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Unmuted" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Unmuted" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Unmuted" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Unmuted" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Unmuted" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Unmuted" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Unmuted" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "Unmuted" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Unmuted" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Unmuted" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Unmuted" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Unmuted" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Unmuted" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Unmuted" } } } }, "Upgrade to Pro" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Upgrade to Pro" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Upgradovat na verzi Pro" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Auf Pro upgraden" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Upgrade to Pro" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Upgrade to Pro" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Actualice a Pro" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Passer à la version Pro" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Upgrade to Pro" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Passa a Pro" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "프로로 업그레이드" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Ulepsz do wersji Pro" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Upgrade to Pro" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Upgrade to Pro" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Pro sürümüne Yükselt" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Upgrade to Pro" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "升级至 Pro" } } } }, "Use music visualizer spectrogram" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Use music visualizer spectrogram" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Použít hudební vizualizér" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Musik-Visualizer Spektrogramm verwenden" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Use music visualizer spectrogram" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Use music visualiser spectrogram" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Usar espectrograma del visualizador musical" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Utiliser le spectrogramme du visualiseur de musique" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Use music visualizer spectrogram" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Usa spettrogramma per la visualizzatore musicale" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "음악 시각화 스펙트로그램 사용" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Użyj spektrogramu wizualizera muzyki" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Use music visualizer spectrogram" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Use music visualizer spectrogram" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Müzik görselleştirici spektrogram kullanın" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Use music visualizer spectrogram" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "使用音乐可视化器谱图" } } } }, "Use your macOS system accent color" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Use your macOS system accent color" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Use your macOS system accent color" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "macOS-System-Akzentfarbe verwenden" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Use your macOS system accent color" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Use your macOS system accent colour" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Use your macOS system accent color" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Use your macOS system accent color" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Use your macOS system accent color" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Use your macOS system accent color" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "MacOS 시스템 강조 색상 사용" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Użyj koloru akcentu systemu macOS" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Use your macOS system accent color" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Use your macOS system accent color" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Use your macOS system accent color" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Use your macOS system accent color" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Use your macOS system accent color" } } } }, "Using System Accent" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Using System Accent" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Using System Accent" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "System Akzent verwenden" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Using System Accent" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Using System Accent" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Using System Accent" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Using System Accent" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Using System Accent" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Using System Accent" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "시스템 강조 색상 사용" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Korzystanie z akcentu systemowego" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Using System Accent" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Using System Accent" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Using System Accent" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Using System Accent" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Using System Accent" } } } }, "Version" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Version" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Verze" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Version" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Version" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Version" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Versión" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Version" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Version" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Versione" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "버전" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Wersja" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Version" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Version" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Versiyon" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Version" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "版本" } } } }, "Version info" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Version info" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Informace o verzi" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Versionsinfo" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Version info" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Version info" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Información de la versión" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Infos de la version" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Version info" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Info versione" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "버전 설명" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Informacje o wersji" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Version info" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Version info" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Versiyon bilgisi" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Version info" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "版本信息" } } } }, "View on GitHub: pear-devs/pear-desktop" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "View on GitHub: pear-devs/pear-desktop" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "View on GitHub: pear-devs/pear-desktop" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Auf GitHub: pear-devs/pear-desktop anschauen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "View on GitHub: pear-devs/pear-desktop" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "View on GitHub: pear-devs/pear-desktop" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "View on GitHub: pear-devs/pear-desktop" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "View on GitHub: pear-devs/pear-desktop" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "View on GitHub: pear-devs/pear-desktop" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "View on GitHub: pear-devs/pear-desktop" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "GitHub에서 보기: pear-devs/pear-desktop" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Zobacz na GitHub: pear-devs/pear-desktop" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "View on GitHub: pear-devs/pear-desktop" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "View on GitHub: pear-devs/pear-desktop" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "View on GitHub: pear-devs/pear-desktop" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "View on GitHub: pear-devs/pear-desktop" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "View on GitHub: pear-devs/pear-desktop" } } } }, "Welcome" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Welcome" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Vítejte" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Willkommen" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Welcome" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "Welcome" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Bienvenido" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Bienvenue" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Welcome" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Benvenuto" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "환영합니다" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Witamy" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Welcome" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Welcome" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Hoş geldiniz" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Welcome" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "欢迎" } } } }, "What's New" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "What's New" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Co je nového" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Was ist neu" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "What's New" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "What's New" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Novedades" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Nouveautés" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "What's New" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Novità" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "새로운 기능" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Co nowego" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "What's New" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "What's New" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Neler Yeni" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "What's New" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "更新内容" } } } }, "Window Appearance" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Window Appearance" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Window Appearance" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Fenster-Erscheinungsbild" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Window Appearance" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Window Appearance" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Window Appearance" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Window Appearance" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Window Appearance" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Window Appearance" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "창 모양" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Wygląd okna" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Window Appearance" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Window Appearance" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Window Appearance" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Window Appearance" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Window Appearance" } } } }, "Window Behavior" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Window Behavior" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Window Behavior" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Fensterverhalten" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Window Behavior" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Window Behaviour" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Window Behavior" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Window Behavior" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Window Behavior" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Window Behavior" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "창 동작" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Zachowanie okna" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Window Behavior" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Window Behavior" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Window Behavior" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Window Behavior" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Window Behavior" } } } }, "You can now enjoy the app. If you want to tweak things further, you can always visit the settings." : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "You can now enjoy the app. If you want to tweak things further, you can always visit the settings." } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Du kannst die App nun genießen. Wenn du Dinge weiter anpassen möchtest, kannst du dies jederzeit in den Einstellungen tun." } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "You can now enjoy the app. If you want to tweak things further, you can always visit the settings." } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "You can now enjoy the app. If you want to tweak things further, you can always visit the settings." } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Ahora puede disfrutar de la aplicación. Si desea modificar otros detalles, siempre puede visitar los ajustes." } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Vous pouvez maintenant profiter de l'application. Si vous voulez modifier davantage les choses, vous pouvez toujours consulter les paramètres." } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "You can now enjoy the app. If you want to tweak things further, you can always visit the settings." } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Ora puoi goderti l'app. Se vuoi modificare ulteriormente le cose, puoi sempre visitare le impostazioni." } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "이제 앱을 즐기실 수 있습니다. 추가로 설정을 조정하고 싶으시면 언제든지 설정 메뉴를 방문하시면 됩니다." } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Możesz już cieszyć się aplikacją. Jeśli chcesz wprowadzić dodatkowe zmiany, zajrzyj do ustawień." } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "You can now enjoy the app. If you want to tweak things further, you can always visit the settings." } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "You can now enjoy the app. If you want to tweak things further, you can always visit the settings." } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Artık uygulamanın keyfini çıkarabilirsiniz. Daha fazla ayar yapmak isterseniz, her zaman ayarlar bölümünü ziyaret edebilirsiniz." } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "You can now enjoy the app. If you want to tweak things further, you can always visit the settings." } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "如果您想进一步调整内容,您可以随时访问设置。" } } } }, "You're All Set!" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "You're All Set!" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Vše je nastaveno!" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Du bist bereit!" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "You're All Set!" } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "You're All Set!" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "¡Todo listo!" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Tout est prêt!" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "You're All Set!" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "È Tutto ok!" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "준비되었습니다!" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Wszystko gotowe!" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "You're All Set!" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "You're All Set!" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Tamamen Hazırsın!" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "You're All Set!" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "一切都准备好了!" } } } }, "Your macOS system accent color" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "Your macOS system accent color" } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "Your macOS system accent color" } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "Akzentfarbe Ihres macOS-Systems" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Your macOS system accent color" } }, "en-GB" : { "stringUnit" : { "state" : "needs_review", "value" : "Your macOS system accent colour" } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "Your macOS system accent color" } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Your macOS system accent color" } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "Your macOS system accent color" } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "Your macOS system accent color" } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "macOS 시스템 강조 색상" } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "Kolor akcentu twojego systemu macOS" } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "Your macOS system accent color" } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "Your macOS system accent color" } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "Your macOS system accent color" } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "Your macOS system accent color" } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "Your macOS system accent color" } } } }, "YouTube Music requires this third-party app to be installed: " : { "localizations" : { "ar" : { "stringUnit" : { "state" : "needs_review", "value" : "YouTube Music requires this third-party app to be installed: " } }, "cs" : { "stringUnit" : { "state" : "needs_review", "value" : "YouTube Music vyžaduje instalaci aplikace třetí strany: " } }, "de" : { "stringUnit" : { "state" : "needs_review", "value" : "YouTube Musik benötigt diese Drittanbieter-App: " } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "YouTube Music requires this third-party app to be installed: " } }, "en-GB" : { "stringUnit" : { "state" : "translated", "value" : "YouTube Music requires this third-party app to be installed: " } }, "es" : { "stringUnit" : { "state" : "needs_review", "value" : "YouTube Music requiere la instalación de esta aplicación independiente: " } }, "fr" : { "stringUnit" : { "state" : "needs_review", "value" : "YouTube Music requiert l'installation de cette application tierce : " } }, "hu" : { "stringUnit" : { "state" : "needs_review", "value" : "YouTube Music requires this third-party app to be installed: " } }, "it" : { "stringUnit" : { "state" : "needs_review", "value" : "YouTube Music richiede l'installazione di questa app di terze parti: " } }, "ko" : { "stringUnit" : { "state" : "needs_review", "value" : "유튜브 뮤직 서드파티-앱 설치를 필요로 합니다 " } }, "pl" : { "stringUnit" : { "state" : "needs_review", "value" : "YouTube Music wymaga zainstalowania tej aplikacji firm trzecich: " } }, "pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "YouTube Music requires this third-party app to be installed: " } }, "ru" : { "stringUnit" : { "state" : "needs_review", "value" : "YouTube Music requires this third-party app to be installed: " } }, "tr" : { "stringUnit" : { "state" : "needs_review", "value" : "YouTube Music, bu üçüncü taraf uygulamanın yüklenmesini gerektirir: " } }, "uk" : { "stringUnit" : { "state" : "needs_review", "value" : "YouTube Music requires this third-party app to be installed: " } }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "YouTube 音乐需要安装此第三方应用程序: " } } } } }, "version" : "1.0" } ================================================ FILE: boringNotch/MediaControllers/AppleMusicController.swift ================================================ // // AppleMusicController.swift // boringNotch // // Created by Alexander on 2025-03-29. // import Foundation import Combine import SwiftUI class AppleMusicController: MediaControllerProtocol { // MARK: - Properties @Published private var playbackState: PlaybackState = PlaybackState( bundleIdentifier: "com.apple.Music", playbackRate: 1 ) var playbackStatePublisher: AnyPublisher { $playbackState.eraseToAnyPublisher() } var supportsVolumeControl: Bool { return true } var supportsFavorite: Bool { return true } private var notificationTask: Task? // MARK: - Initialization init() { setupPlaybackStateChangeObserver() Task { if isActive() { await updatePlaybackInfo() } } } private func setupPlaybackStateChangeObserver() { notificationTask = Task { @Sendable [weak self] in let notifications = DistributedNotificationCenter.default().notifications( named: NSNotification.Name("com.apple.Music.playerInfo") ) for await _ in notifications { await self?.updatePlaybackInfo() } } } deinit { notificationTask?.cancel() } // MARK: - Protocol Implementation func play() async { await executeCommand("play") } func pause() async { await executeCommand("pause") } func togglePlay() async { await executeCommand("playpause") } func nextTrack() async { await executeCommand("next track") } func previousTrack() async { await executeCommand("previous track") } func seek(to time: Double) async { await executeCommand("set player position to \(time)") await updatePlaybackInfo() } func toggleShuffle() async { await executeCommand("set shuffle enabled to not shuffle enabled") try? await Task.sleep(for: .milliseconds(150)) await updatePlaybackInfo() } func toggleRepeat() async { await executeCommand(""" if song repeat is off then set song repeat to all else if song repeat is all then set song repeat to one else set song repeat to off end if """) try? await Task.sleep(for: .milliseconds(150)) await updatePlaybackInfo() } func setVolume(_ level: Double) async { let clampedLevel = max(0.0, min(1.0, level)) let volumePercentage = Int(clampedLevel * 100) await executeCommand("set sound volume to \(volumePercentage)") try? await Task.sleep(for: .milliseconds(150)) await updatePlaybackInfo() } func isActive() -> Bool { let runningApps = NSWorkspace.shared.runningApplications return runningApps.contains { $0.bundleIdentifier == "com.apple.Music" } } func setFavorite(_ favorite: Bool) async { let script = """ tell application \"Music\" try set favorited of current track to " + (favorite ? "true" : "false") + " end try end tell """ try? await AppleScriptHelper.executeVoid(script) try? await Task.sleep(for: .milliseconds(150)) await updatePlaybackInfo() } func updatePlaybackInfo() async { guard let descriptor = try? await fetchPlaybackInfoAsync() else { return } guard descriptor.numberOfItems >= 11 else { return } var updatedState = self.playbackState updatedState.isPlaying = descriptor.atIndex(1)?.booleanValue ?? false updatedState.title = descriptor.atIndex(2)?.stringValue ?? "Unknown" updatedState.artist = descriptor.atIndex(3)?.stringValue ?? "Unknown" updatedState.album = descriptor.atIndex(4)?.stringValue ?? "Unknown" updatedState.currentTime = descriptor.atIndex(5)?.doubleValue ?? 0 updatedState.duration = descriptor.atIndex(6)?.doubleValue ?? 0 updatedState.isShuffled = descriptor.atIndex(7)?.booleanValue ?? false let repeatModeValue = descriptor.atIndex(8)?.int32Value ?? 0 updatedState.repeatMode = RepeatMode(rawValue: Int(repeatModeValue)) ?? .off let volumePercentage = descriptor.atIndex(9)?.int32Value ?? 50 updatedState.volume = Double(volumePercentage) / 100.0 updatedState.artwork = descriptor.atIndex(10)?.data as Data? let lovedState = descriptor.atIndex(11)?.booleanValue ?? false updatedState.isFavorite = lovedState updatedState.lastUpdated = Date() self.playbackState = updatedState } // MARK: - Private Methods private func executeCommand(_ command: String) async { let script = "tell application \"Music\" to \(command)" try? await AppleScriptHelper.executeVoid(script) } private func fetchPlaybackInfoAsync() async throws -> NSAppleEventDescriptor? { let script = """ tell application "Music" set isRunning to true try set playerState to player state is playing set currentTrackName to name of current track set currentTrackArtist to artist of current track set currentTrackAlbum to album of current track set trackPosition to player position set trackDuration to duration of current track set shuffleState to shuffle enabled set repeatState to song repeat if repeatState is off then set repeatValue to 1 else if repeatState is one then set repeatValue to 2 else if repeatState is all then set repeatValue to 3 end if try set artData to data of artwork 1 of current track on error set artData to "" end try set currentVolume to sound volume set favoriteState to favorited of current track return {playerState, currentTrackName, currentTrackArtist, currentTrackAlbum, trackPosition, trackDuration, shuffleState, repeatValue, currentVolume, artData, favoriteState} on error return {false, "Not Playing", "Unknown", "Unknown", 0, 0, false, 0, 50, "", false} end try end tell """ return try await AppleScriptHelper.execute(script) } } ================================================ FILE: boringNotch/MediaControllers/MediaControllerProtocol.swift ================================================ // // MediaControllerProtocol.swift // boringNotch // // Created by Alexander on 2025-03-29. // import Foundation import AppKit import Combine protocol MediaControllerProtocol: ObservableObject { var playbackStatePublisher: AnyPublisher { get } var supportsVolumeControl: Bool { get } var supportsFavorite: Bool { get } func setFavorite(_ favorite: Bool) async func play() async func pause() async func seek(to time: Double) async func nextTrack() async func previousTrack() async func togglePlay() async func toggleShuffle() async func toggleRepeat() async func setVolume(_ level: Double) async func isActive() -> Bool func updatePlaybackInfo() async } ================================================ FILE: boringNotch/MediaControllers/NowPlayingController.swift ================================================ // // NowPlayingController.swift // boringNotch // // Created by Alexander on 2025-03-29. // import AppKit import Combine import Foundation final class NowPlayingController: ObservableObject, MediaControllerProtocol { func updatePlaybackInfo() async { await fetchFavoriteStateIfSupported() } // MARK: - Properties @Published private(set) var playbackState: PlaybackState = .init( bundleIdentifier: "com.apple.Music" ) var playbackStatePublisher: AnyPublisher { $playbackState.eraseToAnyPublisher() } var supportsVolumeControl: Bool { let bundleID = playbackState.bundleIdentifier return bundleID == "com.apple.Music" || bundleID == "com.spotify.client" } var supportsFavorite: Bool { let bundleID = playbackState.bundleIdentifier return bundleID == "com.apple.Music" } func setFavorite(_ favorite: Bool) async { let bundleID = playbackState.bundleIdentifier if bundleID == "com.apple.Music" { let runningApps = NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.Music") if !runningApps.isEmpty { let script = """ tell application "Music" try set favorited of current track to \(favorite ? "true" : "false") end try end tell """ try? await AppleScriptHelper.executeVoid(script) } } // Update the favorite state locally and fetch updated info try? await Task.sleep(for: .milliseconds(150)) await updatePlaybackInfo() } private var lastMusicItem: (title: String, artist: String, album: String, duration: TimeInterval, artworkData: Data?)? // MARK: - Media Remote Functions private let mediaRemoteBundle: CFBundle private let MRMediaRemoteSendCommandFunction: @convention(c) (Int, AnyObject?) -> Void private let MRMediaRemoteSetElapsedTimeFunction: @convention(c) (Double) -> Void private let MRMediaRemoteSetShuffleModeFunction: @convention(c) (Int) -> Void private let MRMediaRemoteSetRepeatModeFunction: @convention(c) (Int) -> Void private var process: Process? private var pipeHandler: JSONLinesPipeHandler? private var streamTask: Task? // MARK: - Initialization init?() { guard let bundle = CFBundleCreate( kCFAllocatorDefault, NSURL(fileURLWithPath: "/System/Library/PrivateFrameworks/MediaRemote.framework")), let MRMediaRemoteSendCommandPointer = CFBundleGetFunctionPointerForName( bundle, "MRMediaRemoteSendCommand" as CFString), let MRMediaRemoteSetElapsedTimePointer = CFBundleGetFunctionPointerForName( bundle, "MRMediaRemoteSetElapsedTime" as CFString), let MRMediaRemoteSetShuffleModePointer = CFBundleGetFunctionPointerForName( bundle, "MRMediaRemoteSetShuffleMode" as CFString), let MRMediaRemoteSetRepeatModePointer = CFBundleGetFunctionPointerForName( bundle, "MRMediaRemoteSetRepeatMode" as CFString) else { return nil } mediaRemoteBundle = bundle MRMediaRemoteSendCommandFunction = unsafeBitCast( MRMediaRemoteSendCommandPointer, to: (@convention(c) (Int, AnyObject?) -> Void).self) MRMediaRemoteSetElapsedTimeFunction = unsafeBitCast( MRMediaRemoteSetElapsedTimePointer, to: (@convention(c) (Double) -> Void).self) MRMediaRemoteSetShuffleModeFunction = unsafeBitCast( MRMediaRemoteSetShuffleModePointer, to: (@convention(c) (Int) -> Void).self) MRMediaRemoteSetRepeatModeFunction = unsafeBitCast( MRMediaRemoteSetRepeatModePointer, to: (@convention(c) (Int) -> Void).self) Task { await setupNowPlayingObserver() } } deinit { streamTask?.cancel() if let pipeHandler = self.pipeHandler { Task { await pipeHandler.close() } } if let process = self.process { if process.isRunning { process.terminate() process.waitUntilExit() } } self.process = nil self.pipeHandler = nil } // MARK: - Protocol Implementation func play() async { MRMediaRemoteSendCommandFunction(0, nil) } func pause() async { MRMediaRemoteSendCommandFunction(1, nil) } func togglePlay() async { MRMediaRemoteSendCommandFunction(2, nil) } func nextTrack() async { MRMediaRemoteSendCommandFunction(4, nil) } func previousTrack() async { MRMediaRemoteSendCommandFunction(5, nil) } func seek(to time: Double) async { MRMediaRemoteSetElapsedTimeFunction(time) } func isActive() -> Bool { return true } func toggleShuffle() async { // MRMediaRemoteSendCommandFunction(6, nil) MRMediaRemoteSetShuffleModeFunction(playbackState.isShuffled ? 1 : 3) playbackState.isShuffled.toggle() } func toggleRepeat() async { // MRMediaRemoteSendCommandFunction(7, nil) let newRepeatMode = (playbackState.repeatMode == .off) ? 3 : (playbackState.repeatMode.rawValue - 1) playbackState.repeatMode = RepeatMode(rawValue: newRepeatMode) ?? .off MRMediaRemoteSetRepeatModeFunction(newRepeatMode) } func setVolume(_ level: Double) async { // MediaRemote framework doesn't provide direct volume control for the active audio session // As a workaround, try to control the currently active music app directly let clampedLevel = max(0.0, min(1.0, level)) let volumePercentage = Int(clampedLevel * 100) let bundleID = playbackState.bundleIdentifier if !bundleID.isEmpty { if bundleID == "com.apple.Music" { let runningApps = NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.Music") if !runningApps.isEmpty { let script = "tell application \"Music\" to set sound volume to \(volumePercentage)" try? await AppleScriptHelper.executeVoid(script) } } else if bundleID == "com.spotify.client" { let runningApps = NSRunningApplication.runningApplications(withBundleIdentifier: "com.spotify.client") if !runningApps.isEmpty { let script = "tell application \"Spotify\" to set sound volume to \(volumePercentage)" try? await AppleScriptHelper.executeVoid(script) } } } playbackState.volume = clampedLevel } // MARK: - Setup Methods private func setupNowPlayingObserver() async { let process = Process() guard let scriptURL = Bundle.main.url(forResource: "mediaremote-adapter", withExtension: "pl"), let frameworkPath = Bundle.main.privateFrameworksPath?.appending("/MediaRemoteAdapter.framework") else { assertionFailure("Could not find mediaremote-adapter.pl script or framework path") return } process.executableURL = URL(fileURLWithPath: "/usr/bin/perl") process.arguments = [scriptURL.path, frameworkPath, "stream"] let pipeHandler = JSONLinesPipeHandler() process.standardOutput = await pipeHandler.getPipe() self.process = process self.pipeHandler = pipeHandler do { try process.run() streamTask = Task { [weak self] in await self?.processJSONStream() } } catch { assertionFailure("Failed to launch mediaremote-adapter.pl: \(error)") } } // MARK: - Async Stream Processing private func processJSONStream() async { guard let pipeHandler = self.pipeHandler else { return } await pipeHandler.readJSONLines(as: NowPlayingUpdate.self) { [weak self] update in await self?.handleAdapterUpdate(update) } } // MARK: - Update Methods private func handleAdapterUpdate(_ update: NowPlayingUpdate) async { let payload = update.payload let diff = update.diff ?? false var newPlaybackState = PlaybackState(bundleIdentifier: playbackState.bundleIdentifier) newPlaybackState.title = payload.title ?? (diff ? self.playbackState.title : "") newPlaybackState.artist = payload.artist ?? (diff ? self.playbackState.artist : "") newPlaybackState.album = payload.album ?? (diff ? self.playbackState.album : "") newPlaybackState.duration = payload.duration ?? (diff ? self.playbackState.duration : 0) if let elapsedTime = payload.elapsedTime { newPlaybackState.currentTime = elapsedTime } else if diff { if payload.playing == false { let timeSinceLastUpdate = Date().timeIntervalSince(self.playbackState.lastUpdated) newPlaybackState.currentTime = self.playbackState.currentTime + (self.playbackState.playbackRate * timeSinceLastUpdate) } else { newPlaybackState.currentTime = self.playbackState.currentTime } } else { newPlaybackState.currentTime = 0 } if let shuffleMode = payload.shuffleMode { newPlaybackState.isShuffled = shuffleMode != 1 } else if !diff { newPlaybackState.isShuffled = false } else { newPlaybackState.isShuffled = self.playbackState.isShuffled } if let repeatModeValue = payload.repeatMode { newPlaybackState.repeatMode = RepeatMode(rawValue: repeatModeValue) ?? .off } else if !diff { newPlaybackState.repeatMode = .off } else { newPlaybackState.repeatMode = self.playbackState.repeatMode } if let artworkDataString = payload.artworkData { newPlaybackState.artwork = Data( base64Encoded: artworkDataString.trimmingCharacters(in: .whitespacesAndNewlines) ) } else if !diff { newPlaybackState.artwork = nil } if let dateString = payload.timestamp, let date = ISO8601DateFormatter().date(from: dateString) { newPlaybackState.lastUpdated = date } else if !diff { newPlaybackState.lastUpdated = Date() } else { newPlaybackState.lastUpdated = self.playbackState.lastUpdated } newPlaybackState.playbackRate = payload.playbackRate ?? (diff ? self.playbackState.playbackRate : 1.0) newPlaybackState.isPlaying = payload.playing ?? (diff ? self.playbackState.isPlaying : false) newPlaybackState.bundleIdentifier = ( payload.parentApplicationBundleIdentifier ?? payload.bundleIdentifier ?? (diff ? self.playbackState.bundleIdentifier : "") ) newPlaybackState.volume = payload.volume ?? (diff ? self.playbackState.volume : 0.5) self.playbackState = newPlaybackState // Fetch favorite state for supported apps asynchronously // await fetchFavoriteStateIfSupported() } private func fetchFavoriteStateIfSupported() async { let bundleID = playbackState.bundleIdentifier if bundleID == "com.apple.Music" { let runningApps = NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.Music") guard !runningApps.isEmpty else { return } let script = """ tell application "Music" try return favorited of current track on error return false end try end tell """ if let result = try? await AppleScriptHelper.execute(script) { var updated = self.playbackState updated.isFavorite = result.booleanValue self.playbackState = updated } } } } struct NowPlayingUpdate: Codable { let payload: NowPlayingPayload let diff: Bool? } struct NowPlayingPayload: Codable { let title: String? let artist: String? let album: String? let duration: Double? let elapsedTime: Double? let shuffleMode: Int? let repeatMode: Int? let artworkData: String? let timestamp: String? let playbackRate: Double? let playing: Bool? let parentApplicationBundleIdentifier: String? let bundleIdentifier: String? let volume: Double? } actor JSONLinesPipeHandler { private let pipe: Pipe private let fileHandle: FileHandle private var buffer = "" init() { self.pipe = Pipe() self.fileHandle = pipe.fileHandleForReading } func getPipe() -> Pipe { return pipe } func readJSONLines(as type: T.Type, onLine: @escaping (T) async -> Void) async { do { try await self.processLines(as: type) { decodedObject in await onLine(decodedObject) } } catch { print("Error processing JSON stream: \(error)") } } private func processLines(as type: T.Type, onLine: @escaping (T) async -> Void) async throws { while true { let data = try await readData() guard !data.isEmpty else { break } if let chunk = String(data: data, encoding: .utf8) { buffer.append(chunk) while let range = buffer.range(of: "\n") { let line = String(buffer[..(_ line: String, as type: T.Type, onLine: @escaping (T) async -> Void) async { guard let data = line.data(using: .utf8) else { return } do { let decodedObject = try JSONDecoder().decode(T.self, from: data) await onLine(decodedObject) } catch { // Ignore lines that can't be decoded } } private func readData() async throws -> Data { return try await withCheckedThrowingContinuation { continuation in fileHandle.readabilityHandler = { handle in let data = handle.availableData handle.readabilityHandler = nil continuation.resume(returning: data) } } } func close() async { do { fileHandle.readabilityHandler = nil try fileHandle.close() try pipe.fileHandleForWriting.close() } catch { print("Error closing pipe handler: \(error)") } } } ================================================ FILE: boringNotch/MediaControllers/SpotifyController.swift ================================================ // // SpotifyController.swift // boringNotch // // Created by Alexander on 2025-03-29. // import Foundation import Combine import SwiftUI class SpotifyController: MediaControllerProtocol { func setFavorite(_ favorite: Bool) async { //Placeholder } // MARK: - Properties @Published private var playbackState: PlaybackState = PlaybackState( bundleIdentifier: "com.spotify.client" ) var playbackStatePublisher: AnyPublisher { $playbackState.eraseToAnyPublisher() } var supportsVolumeControl: Bool { return true } var supportsFavorite: Bool { false } private var notificationTask: Task? // Constant for time between command and update private let commandUpdateDelay: Duration = .milliseconds(25) private var lastArtworkURL: String? private var artworkFetchTask: Task? init() { setupPlaybackStateChangeObserver() Task { if isActive() { await updatePlaybackInfo() } } } private func setupPlaybackStateChangeObserver() { notificationTask = Task { @Sendable [weak self] in let notifications = DistributedNotificationCenter.default().notifications( named: NSNotification.Name("com.spotify.client.PlaybackStateChanged") ) for await _ in notifications { await self?.updatePlaybackInfo() } } } deinit { notificationTask?.cancel() artworkFetchTask?.cancel() } // MARK: - Protocol Implementation func play() async { await executeCommand("play") } func pause() async { await executeCommand("pause") } func togglePlay() async { await executeCommand("playpause") } func nextTrack() async { await executeCommand("next track") } func previousTrack() async { await executeAndRefresh("previous track") } func seek(to time: Double) async { await executeAndRefresh("set player position to \(time)") } func toggleShuffle() async { await executeAndRefresh("set shuffling to not shuffling") } func toggleRepeat() async { await executeAndRefresh("set repeating to not repeating") } func setVolume(_ level: Double) async { let clampedLevel = max(0.0, min(1.0, level)) let volumePercentage = Int(clampedLevel * 100) await executeCommand("set sound volume to \(volumePercentage)") try? await Task.sleep(for: commandUpdateDelay) await updatePlaybackInfo() } func isActive() -> Bool { NSWorkspace.shared.runningApplications.contains { $0.bundleIdentifier == playbackState.bundleIdentifier } } func updatePlaybackInfo() async { guard let descriptor = try? await fetchPlaybackInfoAsync() else { return } guard descriptor.numberOfItems >= 10 else { return } let isPlaying = descriptor.atIndex(1)?.booleanValue ?? false let currentTrack = descriptor.atIndex(2)?.stringValue ?? "Unknown" let currentTrackArtist = descriptor.atIndex(3)?.stringValue ?? "Unknown" let currentTrackAlbum = descriptor.atIndex(4)?.stringValue ?? "Unknown" let currentTime = descriptor.atIndex(5)?.doubleValue ?? 0 let duration = (descriptor.atIndex(6)?.doubleValue ?? 0)/1000 let isShuffled = descriptor.atIndex(7)?.booleanValue ?? false let isRepeating = descriptor.atIndex(8)?.booleanValue ?? false let volumePercentage = descriptor.atIndex(9)?.int32Value ?? 50 let artworkURL = descriptor.atIndex(10)?.stringValue ?? "" var state = PlaybackState( bundleIdentifier: "com.spotify.client", isPlaying: isPlaying, title: currentTrack, artist: currentTrackArtist, album: currentTrackAlbum, currentTime: currentTime, duration: duration, playbackRate: 1, isShuffled: isShuffled, repeatMode: isRepeating ? .all : .off, lastUpdated: Date(), artwork: nil, volume: Double(volumePercentage) / 100.0 ) if artworkURL == lastArtworkURL, let existingArtwork = self.playbackState.artwork { state.artwork = existingArtwork } playbackState = state if !artworkURL.isEmpty, let url = URL(string: artworkURL) { guard artworkURL != lastArtworkURL || state.artwork == nil else { return } artworkFetchTask?.cancel() let currentState = state artworkFetchTask = Task { do { let data = try await ImageService.shared.fetchImageData(from: url) await MainActor.run { [weak self] in guard let self = self else { return } var updatedState = currentState updatedState.artwork = data self.playbackState = updatedState self.lastArtworkURL = artworkURL self.artworkFetchTask = nil } } catch { await MainActor.run { [weak self] in self?.artworkFetchTask = nil } } } } } // MARK: - Private Methods private func executeCommand(_ command: String) async { let script = "tell application \"Spotify\" to \(command)" try? await AppleScriptHelper.executeVoid(script) } private func executeAndRefresh(_ command: String) async { await executeCommand(command) try? await Task.sleep(for: commandUpdateDelay) await updatePlaybackInfo() } private func fetchPlaybackInfoAsync() async throws -> NSAppleEventDescriptor? { let script = """ tell application "Spotify" set isRunning to true try set playerState to player state is playing set currentTrackName to name of current track set currentTrackArtist to artist of current track set currentTrackAlbum to album of current track set trackPosition to player position set trackDuration to duration of current track set shuffleState to shuffling set repeatState to repeating set currentVolume to sound volume set artworkURL to artwork url of current track return {playerState, currentTrackName, currentTrackArtist, currentTrackAlbum, trackPosition, trackDuration, shuffleState, repeatState, currentVolume, artworkURL} on error return {false, "Unknown", "Unknown", "Unknown", 0, 0, false, false, 50, ""} end try end tell """ return try await AppleScriptHelper.execute(script) } } ================================================ FILE: boringNotch/MediaControllers/YouTube Music Controller/YouTubeMusicAuthentication.swift ================================================ // // YouTubeMusicAuthentication.swift // boringNotch // // Created by Alexander on 2025-09-14. // import Foundation // MARK: - Authentication Manager actor YouTubeMusicAuthManager { private var accessToken: String? private var authenticationTask: Task? private let httpClient: YouTubeMusicHTTPClient init(httpClient: YouTubeMusicHTTPClient) { self.httpClient = httpClient } var currentToken: String? { accessToken } func authenticate() async throws -> String { // Return existing token if valid if let token = accessToken { return token } // Wait for ongoing authentication if in progress if let task = authenticationTask { return try await task.value } // Start new authentication let task = Task { do { let token = try await httpClient.authenticate() await setToken(token) return token } catch { await clearAuthenticationTask() throw error } } authenticationTask = task return try await task.value } func invalidateToken() async { accessToken = nil authenticationTask?.cancel() authenticationTask = nil } private func setToken(_ token: String) async { accessToken = token authenticationTask = nil } private func clearAuthenticationTask() async { authenticationTask = nil } } // MARK: - Authentication State enum AuthenticationState: Sendable { case unauthenticated case authenticating case authenticated(String) case failed(Error) var isAuthenticated: Bool { if case .authenticated = self { return true } return false } var token: String? { if case .authenticated(let token) = self { return token } return nil } } ================================================ FILE: boringNotch/MediaControllers/YouTube Music Controller/YouTubeMusicController.swift ================================================ // // YouTubeMusicController.swift // boringNotch // // Created By Alexander on 2025-03-30. // Modified by Pranav on 2025-06-16. // import Foundation import Combine import SwiftUI final class YouTubeMusicController: MediaControllerProtocol { // MARK: - Published Properties @Published var playbackState = PlaybackState( bundleIdentifier: YouTubeMusicConfiguration.default.bundleIdentifier ) private var artworkFetchTask: Task? var playbackStatePublisher: AnyPublisher { $playbackState.eraseToAnyPublisher() } var supportsVolumeControl: Bool { return true } var supportsFavorite: Bool { true } func setFavorite(_ favorite: Bool) async { do { let token = try await authManager.authenticate() if favorite && !playbackState.isFavorite { _ = try await httpClient.toggleLike(token: token) } else if !favorite && playbackState.isFavorite { _ = try await httpClient.toggleLike(token: token) } try? await Task.sleep(for: .milliseconds(150)) await updatePlaybackInfo() } catch { print("[YouTubeMusicController] Failed to set favorite: \(error)") } } // MARK: - Private Properties private let configuration: YouTubeMusicConfiguration private let httpClient: YouTubeMusicHTTPClient private let authManager: YouTubeMusicAuthManager private var webSocketClient: YouTubeMusicWebSocketClient? private var updateTimer: Timer? private var appStateObserver: Task? private var reconnectDelay: TimeInterval = 1.0 // MARK: - Initialization init(configuration: YouTubeMusicConfiguration = .default) { self.configuration = configuration self.httpClient = YouTubeMusicHTTPClient(baseURL: configuration.baseURL) self.authManager = YouTubeMusicAuthManager(httpClient: httpClient) setupAppStateObserver() Task { await initializeIfAppActive() } } // MARK: - MediaControllerProtocol Implementation func play() async { await sendCommand(endpoint: "/play", method: "POST") } func pause() async { await sendCommand(endpoint: "/pause", method: "POST") } func togglePlay() async { if !isActive() { launchApp() } await sendCommand(endpoint: "/toggle-play", method: "POST") } func nextTrack() async { await sendCommand(endpoint: "/next", method: "POST") } func previousTrack() async { await sendCommand(endpoint: "/previous", method: "POST") } func seek(to time: Double) async { let payload = ["seconds": time] await sendCommand(endpoint: "/seek-to", method: "POST", body: payload) } func setVolume(_ level: Double) async { let clampedLevel = max(0.0, min(1.0, level)) let volumePercentage = Int(clampedLevel * 100) let payload = ["volume": volumePercentage] await sendCommand(endpoint: "/volume", method: "POST", body: payload) } func fetchShuffleState() async { await sendCommand(endpoint: "/shuffle", method: "GET", refresh: false) } func fetchRepeatMode() async { await sendCommand(endpoint: "/repeat-mode", method: "GET", refresh: false) } func toggleShuffle() async { await sendCommand(endpoint: "/shuffle", method: "POST") } func toggleRepeat() async { await sendCommand(endpoint: "/switch-repeat", method: "POST") } nonisolated func isActive() -> Bool { NSWorkspace.shared.runningApplications.contains { $0.bundleIdentifier == configuration.bundleIdentifier } } func updatePlaybackInfo() async { guard isActive() else { resetPlaybackState() return } do { let token = try await authManager.authenticate() let response = try await httpClient.getPlaybackInfo(token: token) await updatePlaybackState(with: response) // Fetch like state if supported do { let likeResp = try await httpClient.getLikeState(token: token) var newState = playbackState if let state = likeResp.state { switch state.uppercased() { case "LIKE": newState.isFavorite = true case "DISLIKE": // We don't have a separate dislike UI yet, treat as not favorited newState.isFavorite = false default: newState.isFavorite = false } } else { newState.isFavorite = false } playbackState = newState } catch { // Don't treat it as an error if the like endpoint doesn't exist — just skip } } catch YouTubeMusicError.authenticationRequired { await authManager.invalidateToken() } catch { print("[YouTubeMusicController] Failed to update playback info: \(error)") } } // MARK: - Private Methods private func setupAppStateObserver() { appStateObserver = Task { [weak self] in await withTaskGroup(of: Void.self) { group in group.addTask { let launchNotifications = NSWorkspace.shared.notificationCenter.notifications( named: NSWorkspace.didLaunchApplicationNotification ) for await notification in launchNotifications { await self?.handleAppLaunched(notification) } } group.addTask { let terminateNotifications = NSWorkspace.shared.notificationCenter.notifications( named: NSWorkspace.didTerminateApplicationNotification ) for await notification in terminateNotifications { await self?.handleAppTerminated(notification) } } } } } private func handleAppLaunched(_ notification: Notification) async { guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication, app.bundleIdentifier == configuration.bundleIdentifier else { return } await initializeIfAppActive() } private func handleAppTerminated(_ notification: Notification) async { guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication, app.bundleIdentifier == configuration.bundleIdentifier else { return } Task { @MainActor in stopPeriodicUpdates() appStateObserver?.cancel() } Task { await webSocketClient?.disconnect() webSocketClient = nil } resetPlaybackState() } private func initializeIfAppActive() async { guard isActive() else { return } do { let token = try await authManager.authenticate() await setupWebSocketIfPossible(token: token) await startPeriodicUpdates() await updatePlaybackInfo() } catch { print("[YouTubeMusicController] Failed to initialize: \(error)") await scheduleReconnect() } } private func setupWebSocketIfPossible(token: String) async { guard let wsURL = WebSocketURLBuilder.buildURL(from: configuration.baseURL) else { print("[YouTubeMusicController] Failed to build WebSocket URL") return } let client = YouTubeMusicWebSocketClient( onMessage: { [weak self] data in await self?.handleWebSocketMessage(data) }, onDisconnect: { [weak self] in await self?.handleWebSocketDisconnect() } ) do { try await client.connect(to: wsURL, with: token) webSocketClient = client stopPeriodicUpdates() // WebSocket will provide real-time updates reconnectDelay = configuration.reconnectDelay.lowerBound } catch { print("[YouTubeMusicController] WebSocket connection failed: \(error)") await scheduleReconnect() } } private func handleWebSocketMessage(_ data: Data) async { guard let message = WebSocketMessage(from: data) else { if let response = try? JSONDecoder().decode(PlaybackResponse.self, from: data) { await updatePlaybackState(with: response) } return } switch message.type { case .playerInfo, .videoChanged, .playerStateChanged: if let data = message.extractData(), let response = PlaybackResponse.from(websocketData: data) { await updatePlaybackState(with: response) } case .positionChanged: guard let data = message.extractData() else { return } var position: Double? = nil if let pos = data["position"] as? Double { position = pos } else if let elapsed = data["elapsedSeconds"] as? Double { position = elapsed } guard let newPosition = position else { return } var copied = playbackState copied.currentTime = newPosition copied.lastUpdated = Date() if copied != playbackState { playbackState = copied } case .repeatChanged: guard let data = message.extractData() else { return } var copy = playbackState if let repeatStr = data["repeat"] as? String { switch repeatStr.uppercased() { case "NONE": copy.repeatMode = .off case "ALL": copy.repeatMode = .all case "ONE": copy.repeatMode = .one default: break } } copy.lastUpdated = Date() if copy != playbackState { playbackState = copy } case .shuffleChanged: guard let data = message.extractData() else { return } var copy = playbackState if let shuffle = data["shuffle"] as? Bool { copy.isShuffled = shuffle } else if let shuffle = data["isShuffled"] as? Bool { copy.isShuffled = shuffle } copy.lastUpdated = Date() if copy != playbackState { playbackState = copy } case .volumeChanged: guard let data = message.extractData() else { return } var copy = playbackState if let volume = data["volume"] as? Double { copy.volume = volume / 100.0 } else if let volume = data["volume"] as? Int { copy.volume = Double(volume) / 100.0 } copy.lastUpdated = Date() if copy != playbackState { playbackState = copy } } } private func handleWebSocketDisconnect() async { webSocketClient = nil await startPeriodicUpdates() // Fallback to polling await scheduleReconnect() } private func scheduleReconnect() async { try? await Task.sleep(for: .seconds(reconnectDelay)) reconnectDelay = min(reconnectDelay * 2, configuration.reconnectDelay.upperBound) if isActive() { await initializeIfAppActive() } } private func startPeriodicUpdates() async { guard isActive() && webSocketClient == nil else { return } stopPeriodicUpdates() updateTimer = Timer.scheduledTimer(withTimeInterval: configuration.updateInterval, repeats: true) { [weak self] _ in Task { @MainActor in await self?.updatePlaybackInfo() } } } private func stopPeriodicUpdates() { updateTimer?.invalidate() updateTimer = nil } func pollPlaybackState() async { if !isActive() { return } await fetchRepeatMode() await fetchShuffleState() await updatePlaybackInfo() } private func sendCommand( endpoint: String, method: String = "POST", body: (any Codable & Sendable)? = nil, refresh: Bool = true ) async { do { let token = try await authManager.authenticate() let data = try await httpClient.sendCommand( endpoint: endpoint, method: method, body: body, token: token ) // Lightweight endpoint-specific parsing if endpoint == "/shuffle" { if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let shuffleState = json["state"] as? Bool { playbackState.isShuffled = shuffleState } else { playbackState.isShuffled = !playbackState.isShuffled } } else if endpoint == "/repeat-mode" { if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { if let mode = json["mode"] as? String { updateRepeatMode(mode) } } } else if endpoint == "/switch-repeat" { // Find next repeat mode let nextMode: RepeatMode switch playbackState.repeatMode { case .off: nextMode = .all case .all: nextMode = .one case .one: nextMode = .off } playbackState.repeatMode = nextMode } else if refresh && webSocketClient == nil { try? await Task.sleep(for: .milliseconds(100)) await updatePlaybackInfo() } } catch YouTubeMusicError.authenticationRequired { await authManager.invalidateToken() } catch { print("[YouTubeMusicController] Command failed: \(error)") } } private func updatePlaybackState(with response: PlaybackResponse) async { var newState = playbackState newState.isPlaying = !response.isPaused if let title = response.title { newState.title = title } if let artist = response.artist { newState.artist = artist } if let album = response.album { newState.album = album } if let elapsed = response.elapsedSeconds { newState.currentTime = elapsed } if let duration = response.songDuration { newState.duration = duration } newState.lastUpdated = Date() if let shuffled = response.isShuffled { newState.isShuffled = shuffled } if let mode = response.repeatMode { switch mode { case 0: newState.repeatMode = .off case 1: newState.repeatMode = .all case 2: newState.repeatMode = .one default: break } } if let volume = response.volume { newState.volume = volume / 100.0 } if newState != playbackState { playbackState = newState artworkFetchTask?.cancel() artworkFetchTask = nil if let artworkURL = response.imageSrc, let url = URL(string: artworkURL) { artworkFetchTask = Task { do { let data = try await ImageService.shared.fetchImageData(from: url) await MainActor.run { [weak self] in self?.playbackState.artwork = data } } catch { /* ignore */ } } } } } private func resetPlaybackState() { playbackState = PlaybackState( bundleIdentifier: configuration.bundleIdentifier, isPlaying: false ) } private func launchApp() { guard let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: configuration.bundleIdentifier) else { return } NSWorkspace.shared.open(url) } private func updateRepeatMode(_ mode: String) { var target: RepeatMode? = nil switch mode { case "NONE": target = .off case "ALL": target = .all case "ONE": target = .one default: break } if let target, target != playbackState.repeatMode { playbackState.repeatMode = target } } } ================================================ FILE: boringNotch/MediaControllers/YouTube Music Controller/YouTubeMusicModels.swift ================================================ // // YouTubeMusicModels.swift // boringNotch // // Created by Alexander on 2025-09-14. // import Foundation // MARK: - Configuration struct YouTubeMusicConfiguration: Sendable { let baseURL: String let bundleIdentifier: String let reconnectDelay: ClosedRange let updateInterval: TimeInterval static let `default` = YouTubeMusicConfiguration( baseURL: "http://localhost:26538", bundleIdentifier: "com.github.th-ch.youtube-music", reconnectDelay: 1...60, updateInterval: 2.0 ) } // MARK: - API Models struct AuthResponse: Decodable, Sendable { let accessToken: String } struct PlaybackResponse: Decodable, Sendable { let isPaused: Bool let title: String? let artist: String? let album: String? let elapsedSeconds: Double? let songDuration: Double? let imageSrc: String? let repeatMode: Int? let isShuffled: Bool? let volume: Double? } // MARK: - WebSocket Message Types enum WebSocketMessageType: String, Sendable { case playerInfo = "PLAYER_INFO" case videoChanged = "VIDEO_CHANGED" case playerStateChanged = "PLAYER_STATE_CHANGED" case positionChanged = "POSITION_CHANGED" case volumeChanged = "VOLUME_CHANGED" case repeatChanged = "REPEAT_CHANGED" case shuffleChanged = "SHUFFLE_CHANGED" } struct WebSocketMessage { let type: WebSocketMessageType let rawData: Data private let parsedJSON: [String: Any]? init?(from data: Data) { let json = (try? JSONSerialization.jsonObject(with: data) as? [String: Any]) guard let typeString = json?["type"] as? String, let messageType = WebSocketMessageType(rawValue: typeString) else { return nil } self.type = messageType self.rawData = data self.parsedJSON = json } func extractData() -> [String: Any]? { parsedJSON } } // MARK: - Extensions extension PlaybackResponse { static func from(websocketData: [String: Any]) -> PlaybackResponse? { let songData = websocketData["song"] as? [String: Any] let isPaused: Bool if let paused = songData?["isPaused"] as? Bool { isPaused = paused } else if let playing = websocketData["isPlaying"] as? Bool { isPaused = !playing } else { isPaused = true } let title = (songData?["title"] as? String) ?? (songData?["alternativeTitle"] as? String) ?? (websocketData["title"] as? String) let artist = (songData?["artist"] as? String) ?? (websocketData["artist"] as? String) let album = songData?["album"] as? String let elapsed = extractDouble(from: songData, key: "elapsedSeconds") ?? extractDouble(from: websocketData, key: "position") let duration = extractDouble(from: songData, key: "songDuration") ?? extractDouble(from: websocketData, key: "songDuration") let imageSrc = (songData?["imageSrc"] as? String) ?? (websocketData["imageSrc"] as? String) let isShuffled = (websocketData["shuffle"] as? Bool) ?? (songData?["isShuffled"] as? Bool) var repeatModeInt: Int? = nil if let repeatVal = websocketData["repeat"] as? String { switch repeatVal.uppercased() { case "NONE": repeatModeInt = 0 case "ALL": repeatModeInt = 1 case "ONE": repeatModeInt = 2 default: break } } else if let repeatStr = songData?["repeat"] as? String { switch repeatStr.uppercased() { case "NONE": repeatModeInt = 0 case "ALL": repeatModeInt = 1 case "ONE": repeatModeInt = 2 default: break } } let volume = extractDouble(from: websocketData, key: "volume") ?? extractDouble(from: songData, key: "volume") return PlaybackResponse( isPaused: isPaused, title: title, artist: artist, album: album, elapsedSeconds: elapsed, songDuration: duration, imageSrc: imageSrc, repeatMode: repeatModeInt, isShuffled: isShuffled, volume: volume ) } func with(elapsedSeconds: Double) -> PlaybackResponse { PlaybackResponse( isPaused: isPaused, title: title, artist: artist, album: album, elapsedSeconds: elapsedSeconds, songDuration: songDuration, imageSrc: imageSrc, repeatMode: repeatMode, isShuffled: isShuffled, volume: volume ) } } private func extractDouble(from dict: [String: Any]?, key: String) -> Double? { guard let dict = dict else { return nil } if let value = dict[key] as? Double { return value } else if let value = dict[key] as? Int { return Double(value) } return nil } ================================================ FILE: boringNotch/MediaControllers/YouTube Music Controller/YouTubeMusicNetworking.swift ================================================ // // YouTubeMusicNetworking.swift // boringNotch // // Created by Alexander on 2025-09-14. // import Foundation // MARK: - HTTP Client final class YouTubeMusicHTTPClient: ObservableObject { private let session: URLSession private let baseURL: String private static let decoder = JSONDecoder() private static let encoder = JSONEncoder() init(baseURL: String) { self.baseURL = baseURL let config = URLSessionConfiguration.default config.requestCachePolicy = .reloadIgnoringLocalCacheData config.urlCache = nil config.timeoutIntervalForRequest = 5 config.timeoutIntervalForResource = 10 self.session = URLSession(configuration: config) } // MARK: - Authentication func authenticate() async throws -> String { guard let url = URL(string: "\(baseURL)/auth/boringNotch") else { throw YouTubeMusicError.invalidURL } var request = URLRequest(url: url) request.httpMethod = "POST" let (data, response) = try await session.data(for: request) try validateResponse(response) let authResponse: AuthResponse = try Self.decoder.decode(AuthResponse.self, from: data) return authResponse.accessToken } // MARK: - Playback Info func getPlaybackInfo(token: String) async throws -> PlaybackResponse { let data = try await sendCommand( endpoint: "/song", method: "GET", token: token ) return try Self.decoder.decode(PlaybackResponse.self, from: data) } // MARK: - Like / Favourites struct LikeStateResponse: Decodable, Sendable { let state: String? } func getLikeState(token: String) async throws -> LikeStateResponse { let data = try await sendCommand(endpoint: "/like-state", method: "GET", token: token) return try Self.decoder.decode(LikeStateResponse.self, from: data) } func toggleLike(token: String) async throws -> Data { return try await sendCommand(endpoint: "/like", method: "POST", token: token) } func toggleDislike(token: String) async throws -> Data { return try await sendCommand(endpoint: "/dislike", method: "POST", token: token) } // MARK: - Commands func sendCommand( endpoint: String, method: String = "POST", body: (any Codable & Sendable)? = nil, token: String ) async throws -> Data { let request = try createAuthenticatedRequest( endpoint: "/api/v1\(endpoint)", method: method, body: body, token: token ) let (data, response) = try await session.data(for: request) try validateResponse(response) return data } // MARK: - Private Helpers private func createAuthenticatedRequest( endpoint: String, method: String, body: (any Codable & Sendable)? = nil, token: String ) throws -> URLRequest { guard let url = URL(string: "\(baseURL)\(endpoint)") else { throw YouTubeMusicError.invalidURL } var request = URLRequest(url: url) request.httpMethod = method request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") if let body = body { request.httpBody = try Self.encoder.encode(body) request.setValue("application/json", forHTTPHeaderField: "Content-Type") } return request } private func validateResponse(_ response: URLResponse) throws { guard let httpResponse = response as? HTTPURLResponse else { throw YouTubeMusicError.invalidResponse } switch httpResponse.statusCode { case 200..<300: break case 401, 403: throw YouTubeMusicError.authenticationRequired default: throw YouTubeMusicError.httpError(httpResponse.statusCode) } } } // MARK: - WebSocket Client actor YouTubeMusicWebSocketClient { private var task: URLSessionWebSocketTask? private let session: URLSession private let onMessage: @Sendable (Data) async -> Void private let onDisconnect: @Sendable () async -> Void var isConnected: Bool { task != nil } init( onMessage: @escaping @Sendable (Data) async -> Void, onDisconnect: @escaping @Sendable () async -> Void, session: URLSession = .shared ) { self.onMessage = onMessage self.onDisconnect = onDisconnect self.session = session } func connect(to url: URL, with token: String) async throws { await disconnect() var request = URLRequest(url: url) request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") let newTask = session.webSocketTask(with: request) task = newTask newTask.resume() Task { await listenForMessages() } } func disconnect() async { task?.cancel(with: .goingAway, reason: nil) task = nil } private func listenForMessages() async { guard let currentTask = task else { return } while !Task.isCancelled && task != nil { do { let message = try await currentTask.receive() let data: Data switch message { case .data(let d): data = d case .string(let s): data = s.data(using: .utf8) ?? Data() @unknown default: continue } await onMessage(data) } catch { break } } task = nil await onDisconnect() } } // MARK: - WebSocket URL Helper struct WebSocketURLBuilder { static func buildURL(from baseURL: String) -> URL? { guard var components = URLComponents(string: baseURL) else { return nil } switch components.scheme { case "http": components.scheme = "ws" case "https": components.scheme = "wss" default: break } components.path = "/api/v1/ws" return components.url } } // MARK: - Errors enum YouTubeMusicError: Error, LocalizedError, Sendable { case invalidURL case invalidResponse case httpError(Int) case authenticationRequired case webSocketNotConnected case encodingFailed case decodingFailed var errorDescription: String? { switch self { case .invalidURL: return "Invalid URL" case .invalidResponse: return "Invalid response" case .httpError(let code): return "HTTP error: \(code)" case .authenticationRequired: return "Authentication required" case .webSocketNotConnected: return "WebSocket not connected" case .encodingFailed: return "Failed to encode data" case .decodingFailed: return "Failed to decode data" } } } ================================================ FILE: boringNotch/Preview Content/Preview Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: boringNotch/Providers/CalendarServiceProviding.swift ================================================ // // CalendarServiceProvider.swift // Calendr // // Created by Paker on 31/12/20. // Original source: Original source: https://github.com/pakerwreah/Calendr // Modified by Alexander on 08/06/25 // import Foundation @preconcurrency import EventKit protocol CalendarServiceProviding { func requestAccess(to type: EKEntityType) async throws -> Bool func calendars() async -> [CalendarModel] func events(from start: Date, to end: Date, calendars: [String]) async -> [EventModel] } class CalendarService: CalendarServiceProviding { private let store = EKEventStore() @MainActor func requestAccess(to type: EKEntityType) async throws -> Bool { if #available(macOS 14.0, *) { switch type { case .event: return try await store.requestFullAccessToEvents() case .reminder: return try await store.requestFullAccessToReminders() @unknown default: return false } } else { return try await store.requestAccess(to: type) } } private func hasAccess(to entityType: EKEntityType) -> Bool { let status = EKEventStore.authorizationStatus(for: entityType) if #available(macOS 14.0, *) { return status == .fullAccess } else { return status == .authorized } } func calendars() async -> [CalendarModel] { var calendars: [EKCalendar] = [] for type in [EKEntityType.event, .reminder] where hasAccess(to: type) { calendars.append(contentsOf: store.calendars(for: type)) } return calendars.map { CalendarModel(from: $0) } } func events(from start: Date, to end: Date, calendars ids: [String]) async -> [EventModel] { let allCalendars = await self.calendars() let filteredCalendars = allCalendars.filter { ids.isEmpty || ids.contains($0.id) } let ekCalendars = filteredCalendars.compactMap { calendarModel in store.calendars(for: .event).first { $0.calendarIdentifier == calendarModel.id } ?? store.calendars(for: .reminder).first { $0.calendarIdentifier == calendarModel.id } } var events: [EventModel] = [] // Fetch regular events if hasAccess(to: .event) { let eventCalendars = ekCalendars.filter { store.calendars(for: .event).contains($0) } let predicate = store.predicateForEvents(withStart: start, end: end, calendars: eventCalendars) let ekEvents = store.events(matching: predicate) events.append(contentsOf: ekEvents.compactMap { EventModel(from: $0) }) } // Fetch reminders if hasAccess(to: .reminder) { let reminderCalendars = ekCalendars.filter { store.calendars(for: .reminder).contains($0) } events.append(contentsOf: await fetchReminders(from: start, to: end, calendars: reminderCalendars)) } return events.sorted { $0.start < $1.start } } private func fetchReminders(from start: Date, to end: Date, calendars: [EKCalendar]) async -> [EventModel] { return await withCheckedContinuation { continuation in // Create predicate for reminders with due dates in the specified range let predicate = store.predicateForReminders(in: calendars) store.fetchReminders(matching: predicate) { reminders in let filteredReminders = (reminders ?? []).filter { reminder in // Check if reminder has a due date within our range guard let dueDate = reminder.dueDateComponents?.date else { return false } return dueDate >= start && dueDate <= end } // Convert to EventModel let eventModels = filteredReminders.compactMap { reminder in EventModel(from: reminder) } continuation.resume(returning: eventModels) } } } func setReminderCompleted(reminderID: String, completed: Bool) async { guard let reminder = store.calendarItem(withIdentifier: reminderID) as? EKReminder else { return } reminder.isCompleted = completed do { try store.save(reminder, commit: true) } catch { print("Failed to update reminder completion: \(error)") } } } // MARK: - Model Extensions extension CalendarModel { init(from calendar: EKCalendar) { self.init( id: calendar.calendarIdentifier, account: calendar.accountTitle, title: calendar.title, color: calendar.color, isSubscribed: calendar.isSubscribed || calendar.isDelegate, isReminder: calendar.allowedEntityTypes.contains(.reminder) ) } } extension EventModel { init?(from event: EKEvent) { guard let calendar = event.calendar else { return nil } self.init( id: event.calendarItemIdentifier, start: event.startDate, end: event.endDate, title: event.title ?? "", location: event.location, notes: event.notes, url: event.url, isAllDay: event.shouldBeAllDay, type: .init(from: event), calendar: .init(from: calendar), participants: .init(from: event), timeZone: calendar.isSubscribed || calendar.isDelegate ? nil : event.timeZone, hasRecurrenceRules: event.hasRecurrenceRules || event.isDetached, priority: nil ) } init?(from reminder: EKReminder) { guard let calendar = reminder.calendar, let dueDateComponents = reminder.dueDateComponents, let date = Calendar.current.date(from: dueDateComponents) else { return nil } self.init( id: reminder.calendarItemIdentifier, start: date, end: Calendar.current.endOfDay(for: date), title: reminder.title ?? "", location: reminder.location, notes: reminder.notes, url: reminder.url, isAllDay: dueDateComponents.hour == nil, type: .reminder(completed: reminder.isCompleted), calendar: .init(from: calendar), participants: [], timeZone: calendar.isSubscribed || calendar.isDelegate ? nil : reminder.timeZone, hasRecurrenceRules: reminder.hasRecurrenceRules, priority: .init(from: reminder.priority) ) } } extension EventType { init(from event: EKEvent) { self = event.birthdayContactIdentifier != nil ? .birthday : .event(.init(from: event.currentUser?.participantStatus)) } } extension AttendanceStatus { init(from status: EKParticipantStatus?) { switch status { case .accepted: self = .accepted case .tentative: self = .maybe case .declined: self = .declined case .pending: self = .pending default: self = .unknown } } } extension Array where Element == Participant { init(from event: EKEvent) { var participants = event.attendees ?? [] if let organizer = event.organizer, !participants.contains(where: { $0.url == organizer.url }) { participants.append(organizer) } self.init( participants.map { .init(from: $0, isOrganizer: $0.url == event.organizer?.url) } ) } } extension Participant { init(from participant: EKParticipant, isOrganizer: Bool) { self.init( name: participant.name ?? participant.url.absoluteString.replacingOccurrences(of: "mailto:", with: ""), status: .init(from: participant.participantStatus), isOrganizer: isOrganizer, isCurrentUser: participant.isCurrentUser ) } } extension Priority { init?(from p: Int) { switch p { case 1...4: self = .high case 5: self = .medium case 6...9: self = .low default: return nil } } } // MARK: - Helper Extensions private extension EKCalendar { var accountTitle: String { switch source.sourceType { case .local, .subscribed, .birthdays: return "Other" default: return source.title } } var isDelegate: Bool { if #available(macOS 13.0, *) { return source.isDelegate } else { return false } } } private extension EKEvent { var currentUser: EKParticipant? { attendees?.first(where: \.isCurrentUser) } var shouldBeAllDay: Bool { guard !isAllDay else { return true } let calendar = Calendar.current let startOfDay = calendar.startOfDay(for: startDate) let endOfDay = calendar.dateInterval(of: .day, for: endDate)?.end return startDate == startOfDay && endDate == endOfDay } } private extension Calendar { func endOfDay(for date: Date) -> Date { dateInterval(of: .day, for: date)?.end ?? date } } ================================================ FILE: boringNotch/Shortcuts/ShortcutConstants.swift ================================================ // // Constants.swift // boringNotch // // Created by Richard Kunkli on 16/08/2024. // import KeyboardShortcuts import SwiftUI extension KeyboardShortcuts.Name { static let clipboardHistoryPanel = Self("clipboardHistoryPanel", default: .init(.c, modifiers: [.shift, .command])) static let toggleMicrophone = Self("toggleMicrophone", default: .init(.f5, modifiers: [.function])) static let decreaseBacklight = Self("decreaseBacklight", default: .init(.f1, modifiers: [.command])) static let increaseBacklight = Self("increaseBacklight", default: .init(.f2, modifiers: [.command])) static let toggleSneakPeek = Self("toggleSneakPeek", default: .init(.h, modifiers: [.command, .shift])) static let toggleNotchOpen = Self("toggleNotchOpen", default: .init(.i, modifiers: [.command, .shift])) } ================================================ FILE: boringNotch/XPCHelperClient/BoringNotchXPCHelperProtocol.swift ================================================ // // BoringNotchXPCHelperProtocol.swift // BoringNotchXPCHelper // // Created by Alexander on 2025-11-16. // import Foundation /// The protocol that this service will vend as its API. This protocol will also need to be visible to the process hosting the service. @objc protocol BoringNotchXPCHelperProtocol { func isAccessibilityAuthorized(with reply: @escaping (Bool) -> Void) func requestAccessibilityAuthorization() func ensureAccessibilityAuthorization(_ promptIfNeeded: Bool, with reply: @escaping (Bool) -> Void) // Keyboard backlight / CoreBrightness access (performed by the helper) func isKeyboardBrightnessAvailable(with reply: @escaping (Bool) -> Void) func currentKeyboardBrightness(with reply: @escaping (NSNumber?) -> Void) func setKeyboardBrightness(_ value: Float, with reply: @escaping (Bool) -> Void) // Screen brightness access (performed by the helper) func isScreenBrightnessAvailable(with reply: @escaping (Bool) -> Void) func currentScreenBrightness(with reply: @escaping (NSNumber?) -> Void) func setScreenBrightness(_ value: Float, with reply: @escaping (Bool) -> Void) } ================================================ FILE: boringNotch/XPCHelperClient/XPCHelperClient.swift ================================================ import Foundation import Cocoa import AsyncXPCConnection final class XPCHelperClient: NSObject { nonisolated static let shared = XPCHelperClient() private let serviceName = "theboringteam.boringnotch.BoringNotchXPCHelper" private var remoteService: RemoteXPCService? private var connection: NSXPCConnection? private var lastKnownAuthorization: Bool? private var monitoringTask: Task? deinit { connection?.invalidate() stopMonitoringAccessibilityAuthorization() } // MARK: - Connection Management (Main Actor Isolated) @MainActor private func ensureRemoteService() -> RemoteXPCService { if let existing = remoteService { return existing } let conn = NSXPCConnection(serviceName: serviceName) conn.interruptionHandler = { [weak self] in Task { @MainActor in self?.connection = nil self?.remoteService = nil } } conn.invalidationHandler = { [weak self] in Task { @MainActor in self?.connection = nil self?.remoteService = nil } } conn.resume() let service = RemoteXPCService( connection: conn, remoteInterface: BoringNotchXPCHelperProtocol.self ) connection = conn remoteService = service return service } @MainActor private func getRemoteService() -> RemoteXPCService? { remoteService } @MainActor private func notifyAuthorizationChange(_ granted: Bool) { guard lastKnownAuthorization != granted else { return } lastKnownAuthorization = granted NotificationCenter.default.post( name: .accessibilityAuthorizationChanged, object: nil, userInfo: ["granted": granted] ) } // MARK: - Monitoring nonisolated func startMonitoringAccessibilityAuthorization(every interval: TimeInterval = 3.0) { // Ensure only one monitor exists stopMonitoringAccessibilityAuthorization() monitoringTask = Task.detached { [weak self] in guard let self = self else { return } while !Task.isCancelled { // Call the helper method periodically which will notify on change _ = await self.isAccessibilityAuthorized() do { try await Task.sleep(for: .seconds(interval)) } catch { break } } } } nonisolated func stopMonitoringAccessibilityAuthorization() { monitoringTask?.cancel() monitoringTask = nil } // Expose whether the client is actively monitoring (useful for tests/debug) var isMonitoring: Bool { return monitoringTask != nil } // MARK: - Accessibility nonisolated func requestAccessibilityAuthorization() { Task { let service = await MainActor.run { ensureRemoteService() } try? await service.withService { service in service.requestAccessibilityAuthorization() } } } nonisolated func isAccessibilityAuthorized() async -> Bool { do { let service = await MainActor.run { ensureRemoteService() } let result: Bool = try await service.withContinuation { service, continuation in service.isAccessibilityAuthorized { authorized in continuation.resume(returning: authorized) } } await MainActor.run { notifyAuthorizationChange(result) } return result } catch { return false } } nonisolated func ensureAccessibilityAuthorization(promptIfNeeded: Bool) async -> Bool { do { let service = await MainActor.run { ensureRemoteService() } let result: Bool = try await service.withContinuation { service, continuation in service.ensureAccessibilityAuthorization(promptIfNeeded) { authorized in continuation.resume(returning: authorized) } } await MainActor.run { notifyAuthorizationChange(result) } return result } catch { return false } } // MARK: - Keyboard Brightness nonisolated func isKeyboardBrightnessAvailable() async -> Bool { do { let service = await MainActor.run { ensureRemoteService() } return try await service.withContinuation { service, continuation in service.isKeyboardBrightnessAvailable { available in continuation.resume(returning: available) } } } catch { return false } } nonisolated func currentKeyboardBrightness() async -> Float? { do { let service = await MainActor.run { ensureRemoteService() } let result: NSNumber? = try await service.withContinuation { service, continuation in service.currentKeyboardBrightness { value in continuation.resume(returning: value) } } return result?.floatValue } catch { return nil } } nonisolated func setKeyboardBrightness(_ value: Float) async -> Bool { do { let service = await MainActor.run { ensureRemoteService() } return try await service.withContinuation { service, continuation in service.setKeyboardBrightness(value) { success in continuation.resume(returning: success) } } } catch { return false } } // MARK: - Screen Brightness nonisolated func isScreenBrightnessAvailable() async -> Bool { do { let service = await MainActor.run { ensureRemoteService() } return try await service.withContinuation { service, continuation in service.isScreenBrightnessAvailable { available in continuation.resume(returning: available) } } } catch { return false } } nonisolated func currentScreenBrightness() async -> Float? { do { let service = await MainActor.run { ensureRemoteService() } let result: NSNumber? = try await service.withContinuation { service, continuation in service.currentScreenBrightness { value in continuation.resume(returning: value) } } return result?.floatValue } catch { return nil } } nonisolated func setScreenBrightness(_ value: Float) async -> Bool { do { let service = await MainActor.run { ensureRemoteService() } return try await service.withContinuation { service, continuation in service.setScreenBrightness(value) { success in continuation.resume(returning: success) } } } catch { return false } } } extension Notification.Name { static let accessibilityAuthorizationChanged = Notification.Name("accessibilityAuthorizationChanged") } ================================================ FILE: boringNotch/animations/HelloAnimation.swift ================================================ // // HelloAnimation.swift // boringNotch // // Created by Harsh Vardhan Goswami on 08/08/24. // import SwiftUI struct HelloShape: Shape { func path(in rect: CGRect) -> Path { var path = Path() let width = rect.size.width let height = rect.size.height path.move(to: CGPoint(x: 0.00095*width, y: 0.88718*height)) path.addCurve(to: CGPoint(x: 0.19536*width, y: 0.31015*height), control1: CGPoint(x: 0.00993*width, y: 0.87738*height), control2: CGPoint(x: 0.16556*width, y: 0.56785*height)) path.addCurve(to: CGPoint(x: 0.15043*width, y: 0.04964*height), control1: CGPoint(x: 0.22517*width, y: 0.05245*height), control2: CGPoint(x: 0.1859*width, y: -0.068*height)) path.addCurve(to: CGPoint(x: 0.10028*width, y: 0.932*height), control1: CGPoint(x: 0.11495*width, y: 0.16729*height), control2: CGPoint(x: 0.09792*width, y: 1.02023*height)) path.addCurve(to: CGPoint(x: 0.18354*width, y: 0.47822*height), control1: CGPoint(x: 0.10265*width, y: 0.84376*height), control2: CGPoint(x: 0.12157*width, y: 0.47822*height)) path.addCurve(to: CGPoint(x: 0.22327*width, y: 0.88718*height), control1: CGPoint(x: 0.25733*width, y: 0.51463*height), control2: CGPoint(x: 0.19915*width, y: 0.81575*height)) path.addCurve(to: CGPoint(x: 0.38553*width, y: 0.71351*height), control1: CGPoint(x: 0.2474*width, y: 0.95861*height), control2: CGPoint(x: 0.33586*width, y: 0.89978*height)) path.addCurve(to: CGPoint(x: 0.35998*width, y: 0.45441*height), control1: CGPoint(x: 0.43519*width, y: 0.52724*height), control2: CGPoint(x: 0.38978*width, y: 0.4306*height)) path.addCurve(to: CGPoint(x: 0.35478*width, y: 0.87317*height), control1: CGPoint(x: 0.33018*width, y: 0.47822*height), control2: CGPoint(x: 0.27956*width, y: 0.71631*height)) path.addCurve(to: CGPoint(x: 0.53453*width, y: 0.62808*height), control1: CGPoint(x: 0.42999*width, y: 1.03004*height), control2: CGPoint(x: 0.51892*width, y: 0.6939*height)) path.addCurve(to: CGPoint(x: 0.57332*width, y: 0.00623*height), control1: CGPoint(x: 0.55014*width, y: 0.56225*height), control2: CGPoint(x: 0.63955*width, y: 0.05805*height)) path.addCurve(to: CGPoint(x: 0.48723*width, y: 0.60146*height), control1: CGPoint(x: 0.5071*width, y: -0.04559*height), control2: CGPoint(x: 0.48486*width, y: 0.50623*height)) path.addCurve(to: CGPoint(x: 0.54588*width, y: 0.91239*height), control1: CGPoint(x: 0.48959*width, y: 0.6967*height), control2: CGPoint(x: 0.50378*width, y: 0.87597*height)) path.addCurve(to: CGPoint(x: 0.70719*width, y: 0.51043*height), control1: CGPoint(x: 0.58798*width, y: 0.9488*height), control2: CGPoint(x: 0.6807*width, y: 0.64768*height)) path.addCurve(to: CGPoint(x: 0.73273*width, y: 0.01323*height), control1: CGPoint(x: 0.73368*width, y: 0.37317*height), control2: CGPoint(x: 0.76679*width, y: 0.03984*height)) path.addCurve(to: CGPoint(x: 0.67171*width, y: 0.15048*height), control1: CGPoint(x: 0.69868*width, y: -0.01338*height), control2: CGPoint(x: 0.68259*width, y: 0.07205*height)) path.addCurve(to: CGPoint(x: 0.69678*width, y: 0.92639*height), control1: CGPoint(x: 0.66083*width, y: 0.22892*height), control2: CGPoint(x: 0.62204*width, y: 0.86057*height)) path.addCurve(to: CGPoint(x: 0.87275*width, y: 0.47822*height), control1: CGPoint(x: 0.77152*width, y: 0.99222*height), control2: CGPoint(x: 0.78855*width, y: 0.42997*height)) path.addCurve(to: CGPoint(x: 0.91438*width, y: 0.89776*height), control1: CGPoint(x: 0.9734*width, y: 0.51043*height), control2: CGPoint(x: 0.92329*width, y: 0.85998*height)) path.addCurve(to: CGPoint(x: 0.79943*width, y: 0.69608*height), control1: CGPoint(x: 0.87047*width, y: 1.08403*height), control2: CGPoint(x: 0.77956*width, y: height)) path.addCurve(to: CGPoint(x: 0.92006*width, y: 0.53081*height), control1: CGPoint(x: 0.81523*width, y: 0.45436*height), control2: CGPoint(x: 0.86282*width, y: 0.43277*height)) path.addCurve(to: CGPoint(x: 0.99905*width, y: 0.432*height), control1: CGPoint(x: 0.95979*width, y: 0.57703*height), control2: CGPoint(x: 0.98959*width, y: 0.4944*height)) return path } } extension ShapeStyle where Self == AngularGradient { static var hello: some ShapeStyle { LinearGradient( stops: [ .init(color: .blue, location: 0.0), .init(color: .purple, location: 0.2), .init(color: .red, location: 0.4), .init(color: .mint, location: 0.5), .init(color: .indigo, location: 0.7), .init(color: .pink, location: 0.9), .init(color: .blue, location: 1.0) ], startPoint: .leading, endPoint: .trailing ) } } struct GlowingSnake< Content: Shape, Fill: ShapeStyle >: View, Animatable { var progress: Double var delay: Double = 1.0 var fill: Fill var lineWidth = 4.0 var blurRadius = 8.0 @ViewBuilder var shape: Content var animatableData: Double { get { progress } set { progress = newValue } } var body: some View { shape .trim( from: { if progress > 1 - delay { 2 * progress - 1.0 } else if progress > delay { progress - delay } else { .zero } }(), to: progress ) .glow( fill: fill, lineWidth: lineWidth, blurRadius: blurRadius ) } } struct HelloAnimation: View { @State private var progress: Double = 0.0 var onFinish: () -> Void var body: some View { GlowingSnake( progress: progress, fill: .hello, lineWidth: 8, blurRadius: 8.0, shape: { HelloShape() } ) .task { // Wait for the "opening" animation (notch expansion) to complete before starting the snake try? await Task.sleep(for: .seconds(0.6)) withAnimation( .easeInOut(duration: 4.0) ) { progress = 1.0 } // Wait for the animation to complete try? await Task.sleep(for: .seconds(4.0)) onFinish() } } } extension View where Self: Shape { func glow( fill: some ShapeStyle, lineWidth: Double, blurRadius: Double = 8.0, lineCap: CGLineCap = .round ) -> some View { self .stroke(style: StrokeStyle(lineWidth: lineWidth / 2, lineCap: lineCap)) .fill(fill) .overlay { self .stroke(style: StrokeStyle(lineWidth: lineWidth, lineCap: lineCap)) .fill(fill) .blur(radius: blurRadius) } .overlay { self .stroke(style: StrokeStyle(lineWidth: lineWidth, lineCap: lineCap)) .fill(fill) .blur(radius: blurRadius / 2) } } } #Preview { HelloAnimation(onFinish: {}) .frame(width: 300, height: 100) } ================================================ FILE: boringNotch/animations/drop.swift ================================================ // // drop.swift // boringNotch // // Created by Harsh Vardhan Goswami on 04/08/24. // import Foundation import SwiftUI public class BoringAnimations { @Published var notchStyle: Style = .notch init() { self.notchStyle = .notch } var animation: Animation { if #available(macOS 14.0, *), notchStyle == .notch { Animation.spring(.bouncy(duration: 0.4)) } else { Animation.timingCurve(0.16, 1, 0.3, 1, duration: 0.7) } } // TODO: Move all animations to this file } ================================================ FILE: boringNotch/boringNotch.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.automation.apple-events com.apple.security.device.camera com.apple.security.personal-information.calendars com.apple.security.files.bookmarks.app-scope com.apple.security.files.bookmarks.document-scope com.apple.security.files.user-selected.read-write com.apple.security.network.client com.apple.security.network.server com.apple.security.temporary-exception.apple-events com.spotify.client com.apple.Music com.apple.security.temporary-exception.mach-lookup.global-name $(PRODUCT_BUNDLE_IDENTIFIER)-spks $(PRODUCT_BUNDLE_IDENTIFIER)-spki ================================================ FILE: boringNotch/boringNotchApp.swift ================================================ // // boringNotchApp.swift // boringNotchApp // // Created by Harsh Vardhan Goswami on 02/08/24. // import AVFoundation import Combine import Defaults import KeyboardShortcuts import Sparkle import SwiftUI @main struct DynamicNotchApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @Default(.menubarIcon) var showMenuBarIcon @Environment(\.openWindow) var openWindow let updaterController: SPUStandardUpdaterController init() { updaterController = SPUStandardUpdaterController( startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil) // Initialize the settings window controller with the updater controller SettingsWindowController.shared.setUpdaterController(updaterController) } var body: some Scene { MenuBarExtra("boring.notch", systemImage: "sparkle", isInserted: $showMenuBarIcon) { Button("Settings") { SettingsWindowController.shared.showWindow() } .keyboardShortcut(KeyEquivalent(","), modifiers: .command) CheckForUpdatesView(updater: updaterController.updater) Divider() Button("Restart Boring Notch") { ApplicationRelauncher.restart() } Button("Quit", role: .destructive) { NSApplication.shared.terminate(self) } .keyboardShortcut(KeyEquivalent("Q"), modifiers: .command) } } } class AppDelegate: NSObject, NSApplicationDelegate { var statusItem: NSStatusItem? var windows: [String: NSWindow] = [:] // UUID -> NSWindow var viewModels: [String: BoringViewModel] = [:] // UUID -> BoringViewModel var window: NSWindow? let vm: BoringViewModel = .init() @ObservedObject var coordinator = BoringViewCoordinator.shared var quickShareService = QuickShareService.shared var whatsNewWindow: NSWindow? var timer: Timer? var closeNotchTask: Task? private var previousScreens: [NSScreen]? private var onboardingWindowController: NSWindowController? private var screenLockedObserver: Any? private var screenUnlockedObserver: Any? private var isScreenLocked: Bool = false private var windowScreenDidChangeObserver: Any? private var dragDetectors: [String: DragDetector] = [:] // UUID -> DragDetector func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return false } func applicationWillTerminate(_ notification: Notification) { NotificationCenter.default.removeObserver(self) if let observer = screenLockedObserver { DistributedNotificationCenter.default().removeObserver(observer) screenLockedObserver = nil } if let observer = screenUnlockedObserver { DistributedNotificationCenter.default().removeObserver(observer) screenUnlockedObserver = nil } MusicManager.shared.destroy() cleanupDragDetectors() cleanupWindows() XPCHelperClient.shared.stopMonitoringAccessibilityAuthorization() } @MainActor func onScreenLocked(_ notification: Notification) { isScreenLocked = true if !Defaults[.showOnLockScreen] { cleanupWindows() } else { enableSkyLightOnAllWindows() } } @MainActor func onScreenUnlocked(_ notification: Notification) { isScreenLocked = false if !Defaults[.showOnLockScreen] { adjustWindowPosition(changeAlpha: true) } else { disableSkyLightOnAllWindows() } } @MainActor private func enableSkyLightOnAllWindows() { if Defaults[.showOnAllDisplays] { windows.values.forEach { window in if let skyWindow = window as? BoringNotchSkyLightWindow { skyWindow.enableSkyLight() } } } else { if let skyWindow = window as? BoringNotchSkyLightWindow { skyWindow.enableSkyLight() } } } @MainActor private func disableSkyLightOnAllWindows() { // Delay disabling SkyLight to avoid flicker during unlock transition Task { try? await Task.sleep(for: .milliseconds(150)) await MainActor.run { if Defaults[.showOnAllDisplays] { self.windows.values.forEach { window in if let skyWindow = window as? BoringNotchSkyLightWindow { skyWindow.disableSkyLight() } } } else { if let skyWindow = self.window as? BoringNotchSkyLightWindow { skyWindow.disableSkyLight() } } } } } private func cleanupWindows(shouldInvert: Bool = false) { let shouldCleanupMulti = shouldInvert ? !Defaults[.showOnAllDisplays] : Defaults[.showOnAllDisplays] if shouldCleanupMulti { windows.values.forEach { window in window.close() NotchSpaceManager.shared.notchSpace.windows.remove(window) } windows.removeAll() viewModels.removeAll() } else if let window = window { window.close() NotchSpaceManager.shared.notchSpace.windows.remove(window) if let obs = windowScreenDidChangeObserver { NotificationCenter.default.removeObserver(obs) windowScreenDidChangeObserver = nil } self.window = nil } } private func cleanupDragDetectors() { dragDetectors.values.forEach { detector in detector.stopMonitoring() } dragDetectors.removeAll() } private func setupDragDetectors() { cleanupDragDetectors() guard Defaults[.expandedDragDetection] else { return } if Defaults[.showOnAllDisplays] { for screen in NSScreen.screens { setupDragDetectorForScreen(screen) } } else { let preferredScreen: NSScreen? = window?.screen ?? NSScreen.screen(withUUID: coordinator.selectedScreenUUID) ?? NSScreen.main if let screen = preferredScreen { setupDragDetectorForScreen(screen) } } } private func setupDragDetectorForScreen(_ screen: NSScreen) { guard let uuid = screen.displayUUID else { return } let screenFrame = screen.frame let notchHeight = openNotchSize.height let notchWidth = openNotchSize.width // Create notch region at the top-center of the screen where an open notch would occupy let notchRegion = CGRect( x: screenFrame.midX - notchWidth / 2, y: screenFrame.maxY - notchHeight, width: notchWidth, height: notchHeight ) let detector = DragDetector(notchRegion: notchRegion) detector.onDragEntersNotchRegion = { [weak self] in Task { @MainActor in self?.handleDragEntersNotchRegion(onScreen: screen) } } dragDetectors[uuid] = detector detector.startMonitoring() } private func handleDragEntersNotchRegion(onScreen screen: NSScreen) { guard let uuid = screen.displayUUID else { return } if Defaults[.showOnAllDisplays], let viewModel = viewModels[uuid] { viewModel.open() coordinator.currentView = .shelf } else if !Defaults[.showOnAllDisplays], let windowScreen = window?.screen, screen == windowScreen { vm.open() coordinator.currentView = .shelf } } private func createBoringNotchWindow(for screen: NSScreen, with viewModel: BoringViewModel) -> NSWindow { let rect = NSRect(x: 0, y: 0, width: windowSize.width, height: windowSize.height) let styleMask: NSWindow.StyleMask = [.borderless, .nonactivatingPanel, .utilityWindow, .hudWindow] let window = BoringNotchSkyLightWindow(contentRect: rect, styleMask: styleMask, backing: .buffered, defer: false) // Enable SkyLight only when screen is locked if isScreenLocked { window.enableSkyLight() } else { window.disableSkyLight() } window.contentView = NSHostingView( rootView: ContentView() .environmentObject(viewModel) ) window.orderFrontRegardless() NotchSpaceManager.shared.notchSpace.windows.insert(window) // Observe when the window's screen changes so we can update drag detectors windowScreenDidChangeObserver = NotificationCenter.default.addObserver( forName: NSWindow.didChangeScreenNotification, object: window, queue: .main) { [weak self] _ in Task { @MainActor in self?.setupDragDetectors() } } return window } @MainActor private func positionWindow(_ window: NSWindow, on screen: NSScreen, changeAlpha: Bool = false) { if changeAlpha { window.alphaValue = 0 } let screenFrame = screen.frame window.setFrameOrigin( NSPoint( x: screenFrame.origin.x + (screenFrame.width / 2) - window.frame.width / 2, y: screenFrame.origin.y + screenFrame.height - window.frame.height )) window.alphaValue = 1 } func applicationDidFinishLaunching(_ notification: Notification) { NotificationCenter.default.addObserver( self, selector: #selector(screenConfigurationDidChange), name: NSApplication.didChangeScreenParametersNotification, object: nil ) NotificationCenter.default.addObserver( forName: Notification.Name.selectedScreenChanged, object: nil, queue: nil ) { [weak self] _ in Task { @MainActor in self?.adjustWindowPosition(changeAlpha: true) self?.setupDragDetectors() } } NotificationCenter.default.addObserver( forName: Notification.Name.notchHeightChanged, object: nil, queue: nil ) { [weak self] _ in Task { @MainActor in self?.adjustWindowPosition() self?.setupDragDetectors() } } NotificationCenter.default.addObserver( forName: Notification.Name.automaticallySwitchDisplayChanged, object: nil, queue: nil ) { [weak self] _ in guard let self = self, let window = self.window else { return } Task { @MainActor in window.alphaValue = self.coordinator.selectedScreenUUID == self.coordinator.preferredScreenUUID ? 1 : 0 } } NotificationCenter.default.addObserver( forName: Notification.Name.showOnAllDisplaysChanged, object: nil, queue: nil ) { [weak self] _ in Task { @MainActor in guard let self = self else { return } self.cleanupWindows(shouldInvert: true) self.adjustWindowPosition(changeAlpha: true) self.setupDragDetectors() } } NotificationCenter.default.addObserver( forName: Notification.Name.expandedDragDetectionChanged, object: nil, queue: nil ) { [weak self] _ in Task { @MainActor in self?.setupDragDetectors() } } // Use closure-based observers for DistributedNotificationCenter and keep tokens for removal screenLockedObserver = DistributedNotificationCenter.default().addObserver( forName: NSNotification.Name(rawValue: "com.apple.screenIsLocked"), object: nil, queue: .main) { [weak self] notification in Task { @MainActor in self?.onScreenLocked(notification) } } screenUnlockedObserver = DistributedNotificationCenter.default().addObserver( forName: NSNotification.Name(rawValue: "com.apple.screenIsUnlocked"), object: nil, queue: .main) { [weak self] notification in Task { @MainActor in self?.onScreenUnlocked(notification) } } KeyboardShortcuts.onKeyDown(for: .toggleSneakPeek) { [weak self] in guard let self = self else { return } if Defaults[.sneakPeekStyles] == .inline { let newStatus = !self.coordinator.expandingView.show self.coordinator.toggleExpandingView(status: newStatus, type: .music) } else { self.coordinator.toggleSneakPeek( status: !self.coordinator.sneakPeek.show, type: .music, duration: 3.0 ) } } KeyboardShortcuts.onKeyDown(for: .toggleNotchOpen) { [weak self] in Task { [weak self] in guard let self = self else { return } let mouseLocation = NSEvent.mouseLocation var viewModel = self.vm if Defaults[.showOnAllDisplays] { for screen in NSScreen.screens { if screen.frame.contains(mouseLocation) { if let uuid = screen.displayUUID, let screenViewModel = self.viewModels[uuid] { viewModel = screenViewModel break } } } } self.closeNotchTask?.cancel() self.closeNotchTask = nil switch viewModel.notchState { case .closed: await MainActor.run { viewModel.open() } let task = Task { [weak viewModel] in do { try await Task.sleep(for: .seconds(3)) await MainActor.run { viewModel?.close() } } catch { } } self.closeNotchTask = task case .open: await MainActor.run { viewModel.close() } } } } if !Defaults[.showOnAllDisplays] { let viewModel = self.vm let window = createBoringNotchWindow( for: NSScreen.main ?? NSScreen.screens.first!, with: viewModel) self.window = window adjustWindowPosition(changeAlpha: true) } else { adjustWindowPosition(changeAlpha: true) } setupDragDetectors() if coordinator.firstLaunch { DispatchQueue.main.async { self.showOnboardingWindow() } playWelcomeSound() } else if MusicManager.shared.isNowPlayingDeprecated && Defaults[.mediaController] == .nowPlaying { DispatchQueue.main.async { self.showOnboardingWindow(step: .musicPermission) } } previousScreens = NSScreen.screens } func playWelcomeSound() { let audioPlayer = AudioPlayer() audioPlayer.play(fileName: "boring", fileExtension: "m4a") } func deviceHasNotch() -> Bool { if #available(macOS 12.0, *) { for screen in NSScreen.screens { if screen.safeAreaInsets.top > 0 { return true } } } return false } @objc func screenConfigurationDidChange() { let currentScreens = NSScreen.screens let screensChanged = currentScreens.count != previousScreens?.count || Set(currentScreens.compactMap { $0.displayUUID }) != Set(previousScreens?.compactMap { $0.displayUUID } ?? []) || Set(currentScreens.map { $0.frame }) != Set(previousScreens?.map { $0.frame } ?? []) previousScreens = currentScreens if screensChanged { DispatchQueue.main.async { [weak self] in self?.cleanupWindows() self?.adjustWindowPosition() self?.setupDragDetectors() } } } @objc func adjustWindowPosition(changeAlpha: Bool = false) { if Defaults[.showOnAllDisplays] { let currentScreenUUIDs = Set(NSScreen.screens.compactMap { $0.displayUUID }) // Remove windows for screens that no longer exist for uuid in windows.keys where !currentScreenUUIDs.contains(uuid) { if let window = windows[uuid] { window.close() NotchSpaceManager.shared.notchSpace.windows.remove(window) windows.removeValue(forKey: uuid) viewModels.removeValue(forKey: uuid) } } // Create or update windows for all screens for screen in NSScreen.screens { guard let uuid = screen.displayUUID else { continue } if windows[uuid] == nil { let viewModel = BoringViewModel(screenUUID: uuid) let window = createBoringNotchWindow(for: screen, with: viewModel) windows[uuid] = window viewModels[uuid] = viewModel } if let window = windows[uuid], let viewModel = viewModels[uuid] { positionWindow(window, on: screen, changeAlpha: changeAlpha) if viewModel.notchState == .closed { viewModel.close() } } } } else { let selectedScreen: NSScreen if let preferredScreen = NSScreen.screen(withUUID: coordinator.preferredScreenUUID ?? "") { coordinator.selectedScreenUUID = coordinator.preferredScreenUUID ?? "" selectedScreen = preferredScreen } else if Defaults[.automaticallySwitchDisplay], let mainScreen = NSScreen.main, let mainUUID = mainScreen.displayUUID { coordinator.selectedScreenUUID = mainUUID selectedScreen = mainScreen } else { if let window = window { window.alphaValue = 0 } return } vm.screenUUID = selectedScreen.displayUUID vm.notchSize = getClosedNotchSize(screenUUID: selectedScreen.displayUUID) if window == nil { window = createBoringNotchWindow(for: selectedScreen, with: vm) } if let window = window { positionWindow(window, on: selectedScreen, changeAlpha: changeAlpha) if vm.notchState == .closed { vm.close() } } } } @objc func togglePopover(_ sender: Any?) { if window?.isVisible == true { window?.orderOut(nil) } else { window?.orderFrontRegardless() } } @objc func showMenu() { statusItem?.menu?.popUp(positioning: nil, at: NSEvent.mouseLocation, in: nil) } @objc func quitAction() { NSApplication.shared.terminate(self) } private func showOnboardingWindow(step: OnboardingStep = .welcome) { if onboardingWindowController == nil { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 400, height: 600), styleMask: [.titled, .fullSizeContentView], backing: .buffered, defer: false ) window.center() window.title = "Onboarding" window.titlebarAppearsTransparent = true window.titleVisibility = .hidden window.contentView = NSHostingView( rootView: OnboardingView( step: step, onFinish: { window.orderOut(nil) // NSApp.setActivationPolicy(.accessory) window.close() NSApp.deactivate() }, onOpenSettings: { window.close() SettingsWindowController.shared.showWindow() } )) window.isRestorable = false window.identifier = NSUserInterfaceItemIdentifier("OnboardingWindow") onboardingWindowController = NSWindowController(window: window) } // NSApp.setActivationPolicy(.regular) NSApp.activate(ignoringOtherApps: true) onboardingWindowController?.window?.makeKeyAndOrderFront(nil) onboardingWindowController?.window?.orderFrontRegardless() } } extension Notification.Name { static let selectedScreenChanged = Notification.Name("SelectedScreenChanged") static let notchHeightChanged = Notification.Name("NotchHeightChanged") static let showOnAllDisplaysChanged = Notification.Name("showOnAllDisplaysChanged") static let automaticallySwitchDisplayChanged = Notification.Name("automaticallySwitchDisplayChanged") static let expandedDragDetectionChanged = Notification.Name("expandedDragDetectionChanged") } extension CGRect: @retroactive Hashable { public func hash(into hasher: inout Hasher) { hasher.combine(origin.x) hasher.combine(origin.y) hasher.combine(size.width) hasher.combine(size.height) } public static func == (lhs: CGRect, rhs: CGRect) -> Bool { return lhs.origin == rhs.origin && lhs.size == rhs.size } } ================================================ FILE: boringNotch/components/AnimatedFace.swift ================================================ // // AnimatedFace.swift // // Created by Harsh Vardhan Goswami on 04/08/24. // import SwiftUI struct MinimalFaceFeatures: View { @State private var isBlinking = false @State var height:CGFloat = 20; @State var width:CGFloat = 30; var body: some View { VStack(spacing: 4) { // Adjusted spacing to fit within 30x30 // Eyes HStack(spacing: 4) { // Adjusted spacing to fit within 30x30 Eye(isBlinking: $isBlinking) Eye(isBlinking: $isBlinking) } // Nose and mouth combined VStack(spacing: 2) { // Adjusted spacing to fit within 30x30 // Nose RoundedRectangle(cornerRadius: 2) .fill(Color.white) .frame(width: 3, height: 4) // Mouth (happy) GeometryReader { geometry in Path { path in let width = geometry.size.width let height = geometry.size.height path.move(to: CGPoint(x: 0, y: height / 2)) path.addQuadCurve(to: CGPoint(x: width, y: height / 2), control: CGPoint(x: width / 2, y: height)) } .stroke(Color.white, lineWidth: 2) } .frame(width: 14, height: 10) } } .frame(width: self.width, height: self.height) // Maximum size of face .onAppear { startBlinking() } } func startBlinking() { Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { _ in withAnimation(.spring(duration: 0.2)) { isBlinking = true } DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { withAnimation(.spring(duration: 0.2)) { isBlinking = false } } } } } struct Eye: View { @Binding var isBlinking: Bool var body: some View { RoundedRectangle(cornerRadius: 10) .fill(Color.white) .frame(width: 4, height: isBlinking ? 1 : 4) .frame(maxWidth: 15, maxHeight: 15) // Adjusted max size .animation(.easeInOut(duration: 0.1), value: isBlinking) } } struct MinimalFaceFeatures_Previews: PreviewProvider { static var previews: some View { ZStack { Color.black MinimalFaceFeatures() } .previewLayout(.fixed(width: 60, height: 60)) // Adjusted preview size for better visibility } } ================================================ FILE: boringNotch/components/BottomRoundedRectangle.swift ================================================ import SwiftUI struct BottomRoundedRectangle: Shape { var radius: CGFloat func path(in rect: CGRect) -> Path { var path = Path() // Top left corner path.move(to: CGPoint(x: rect.minX, y: rect.minY)) // Top right corner path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY)) // Bottom right corner (rounded) path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - radius)) path.addArc(center: CGPoint(x: rect.maxX - radius, y: rect.maxY - radius), radius: radius, startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 90), clockwise: false) // Bottom left corner (rounded) path.addLine(to: CGPoint(x: rect.minX + radius, y: rect.maxY)) path.addArc(center: CGPoint(x: rect.minX + radius, y: rect.maxY - radius), radius: radius, startAngle: Angle(degrees: 90), endAngle: Angle(degrees: 180), clockwise: false) // Back to top left to close the path path.closeSubpath() return path } } ================================================ FILE: boringNotch/components/Calendar/BoringCalendar.swift ================================================ // // BoringCalendar.swift // boringNotch // // Created by Harsh Vardhan Goswami on 08/09/24. // import Defaults import SwiftUI struct Config: Equatable { // var count: Int = 10 // 3 days past + today + 7 days future var past: Int = 7 var future: Int = 14 var steps: Int = 1 // Each step is one day var spacing: CGFloat = 0 var showsText: Bool = true var offset: Int = 2 // Number of dates to the left of the selected date } struct WheelPicker: View { @EnvironmentObject var vm: BoringViewModel @Binding var selectedDate: Date @State private var scrollPosition: Int? @State private var haptics: Bool = false @State private var byClick: Bool = false let config: Config var body: some View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: config.spacing) { let spacerNum = config.offset let dateCount = totalDateItems() let totalItems = dateCount + 2 * spacerNum ForEach(0..= spacerNum + dateCount { // Leading/trailing spacers sized to match a date cell Spacer() .frame(width: 24, height: 24) .id(index) } else { let date = dateForItemIndex(index: index, spacerNum: spacerNum) let isSelected = Calendar.current.isDate(date, inSameDayAs: selectedDate) dateButton(date: date, isSelected: isSelected, id: index) { selectedDate = date byClick = true withAnimation { scrollPosition = index } if Defaults[.enableHaptics] { haptics.toggle() } } } } } .frame(height: 50) .scrollTargetLayout() } .scrollIndicators(.never) .scrollPosition(id: $scrollPosition, anchor: .center) .scrollTargetBehavior(.viewAligned) // Ensures scroll view snaps the centered view .safeAreaPadding(.horizontal) .sensoryFeedback(.alignment, trigger: haptics) .onChange(of: scrollPosition) { oldValue, newValue in if !byClick { handleScrollChange(newValue: newValue, config: config) } else { byClick = false } } .onAppear { scrollToToday(config: config) } // When parent updates the bound selectedDate (e.g., view reopen), center the wheel on it .onChange(of: selectedDate) { _, newValue in let targetIndex = indexForDate(newValue) if scrollPosition != targetIndex { byClick = true withAnimation { scrollPosition = targetIndex } } } } private func dateButton( date: Date, isSelected: Bool, id: Int, onClick: @escaping () -> Void ) -> some View { let isToday = Calendar.current.isDateInToday(date) return Button(action: onClick) { VStack(spacing: 8) { dayText(date: dateToString(for: date), isToday: isToday, isSelected: isSelected) dateCircle(date: date, isToday: isToday, isSelected: isSelected) } .padding(.vertical, 4) .padding(.horizontal, 4) .background(isSelected ? Color.effectiveAccentBackground : Color.clear) .cornerRadius(8) } .buttonStyle(PlainButtonStyle()) .id(id) } private func dayText(date: String, isToday: Bool, isSelected: Bool) -> some View { Text(date) .font(.caption) .foregroundColor(isSelected ? .white : Color(white: 0.65)) } private func dateCircle(date: Date, isToday: Bool, isSelected: Bool) -> some View { ZStack { Circle() .fill(isToday ? Color.effectiveAccent : .clear) .frame(width: 20, height: 20) .overlay( Circle() .stroke(Color.gray.opacity(0.3), lineWidth: 0) ) Text("\(date.date)") .font(.body) .fontWeight(.medium) .foregroundColor(isSelected ? .white : Color(white: isToday ? 0.9 : 0.65)) } } func handleScrollChange(newValue: Int?, config: Config) { guard let newIndex = newValue else { return } let spacerNum = config.offset let dateCount = totalDateItems() guard (spacerNum..<(spacerNum + dateCount)).contains(newIndex) else { return } let date = dateForItemIndex(index: newIndex, spacerNum: spacerNum) if !Calendar.current.isDate(date, inSameDayAs: selectedDate) { selectedDate = date if Defaults[.enableHaptics] { haptics.toggle() } } } private func scrollToToday(config: Config) { let today = Date() byClick = true scrollPosition = indexForDate(today) selectedDate = today } // MARK: - Index/Date mapping with steps and spacers private func indexForDate(_ date: Date) -> Int { let spacerNum = config.offset let cal = Calendar.current let today = cal.startOfDay(for: Date()) let startDate = cal.startOfDay(for: cal.date(byAdding: .day, value: -config.past, to: today) ?? today) let target = cal.startOfDay(for: date) let days = cal.dateComponents([.day], from: startDate, to: target).day ?? 0 let stepIndex = max(0, min(days / max(config.steps, 1), totalDateItems() - 1)) return spacerNum + stepIndex } private func dateForItemIndex(index: Int, spacerNum: Int) -> Date { let cal = Calendar.current let today = cal.startOfDay(for: Date()) let startDate = cal.date(byAdding: .day, value: -config.past, to: today) ?? today let stepIndex = index - spacerNum return cal.date(byAdding: .day, value: stepIndex * max(config.steps, 1), to: startDate) ?? today } private func totalDateItems() -> Int { let range = config.past + config.future let step = max(config.steps, 1) return Int(ceil(Double(range) / Double(step))) + 1 } private func dateToString(for date: Date) -> String { let formatter = DateFormatter() formatter.dateFormat = "E" return formatter.string(from: date) } } struct CalendarView: View { @EnvironmentObject var vm: BoringViewModel @ObservedObject private var calendarManager = CalendarManager.shared @State private var selectedDate = Date() var body: some View { VStack(spacing: 0) { HStack(alignment: .top, spacing: 8) { VStack(alignment: .leading) { Text(selectedDate.formatted(.dateTime.month(.abbreviated))) .font(.title3) .fontWeight(.semibold) .foregroundColor(.white) Text(selectedDate.formatted(.dateTime.year())) .font(.title3) .fontWeight(.light) .foregroundColor(Color(white: 0.65)) } ZStack(alignment: .top) { WheelPicker(selectedDate: $selectedDate, config: Config()) HStack(alignment: .top) { LinearGradient( colors: [Color.black, .clear], startPoint: .leading, endPoint: .trailing ) .frame(width: 20) Spacer() LinearGradient( colors: [.clear, Color.black], startPoint: .leading, endPoint: .trailing ) .frame(width: 20) } } } let filteredEvents = EventListView.filteredEvents( events: calendarManager.events ) if filteredEvents.isEmpty { EmptyEventsView(selectedDate: selectedDate) Spacer(minLength: 0) } else { EventListView(events: calendarManager.events) } } .listRowBackground(Color.clear) .frame(height: 120) .onChange(of: selectedDate) { Task { await calendarManager.updateCurrentDate(selectedDate) } } .onChange(of: vm.notchState) { _, _ in Task { await calendarManager.updateCurrentDate(Date.now) selectedDate = Date.now } } .onAppear { Task { await calendarManager.updateCurrentDate(Date.now) selectedDate = Date.now } } } } struct EmptyEventsView: View { let selectedDate: Date var body: some View { VStack { Image(systemName: "calendar.badge.checkmark") .font(.title) .foregroundColor(Color(white: 0.65)) Text(Calendar.current.isDateInToday(selectedDate) ? "No events today" : "No events") .font(.subheadline) .foregroundColor(.white) Text("Enjoy your free time!") .font(.caption) .foregroundColor(Color(white: 0.65)) } } } struct EventListView: View { @Environment(\.openURL) private var openURL @ObservedObject private var calendarManager = CalendarManager.shared let events: [EventModel] @Default(.autoScrollToNextEvent) private var autoScrollToNextEvent @Default(.showFullEventTitles) private var showFullEventTitles static func filteredEvents(events: [EventModel]) -> [EventModel] { events.filter { event in if event.type.isReminder { if case .reminder(let completed) = event.type { return !completed || !Defaults[.hideCompletedReminders] } } // Filter out all-day events if setting is enabled if event.isAllDay && Defaults[.hideAllDayEvents] { return false } return true } } private var filteredEvents: [EventModel] { Self.filteredEvents(events: events) } private func scrollToRelevantEvent(proxy: ScrollViewProxy) { let now = Date() // Determine a single target using preferred search order: // 1) first non-all-day upcoming/in-progress event // 2) first all-day event // 3) last event (fallback) let nonAllDayUpcoming = filteredEvents.first(where: { !$0.isAllDay && $0.end > now }) let firstAllDay = filteredEvents.first(where: { $0.isAllDay }) let lastEvent = filteredEvents.last guard let target = nonAllDayUpcoming ?? firstAllDay ?? lastEvent else { return } Task { @MainActor in withTransaction(Transaction(animation: nil)) { proxy.scrollTo(target.id, anchor: .top) } } } var body: some View { ScrollViewReader { proxy in List { ForEach(filteredEvents) { event in Button(action: { if let url = event.calendarAppURL() { openURL(url) } }) { eventRow(event) } .id(event.id) .padding(.leading, -5) .buttonStyle(PlainButtonStyle()) .listRowSeparator(.automatic) .listRowSeparatorTint(.gray.opacity(0.2)) .listRowBackground(Color.clear) } } .listStyle(.plain) .scrollIndicators(.never) .scrollContentBackground(.hidden) .background(Color.clear) .onAppear { scrollToRelevantEvent(proxy: proxy) } .onChange(of: filteredEvents) { _, _ in scrollToRelevantEvent(proxy: proxy) } } Spacer(minLength: 0) } private func eventRow(_ event: EventModel) -> some View { if event.type.isReminder { let isCompleted: Bool if case .reminder(let completed) = event.type { isCompleted = completed } else { isCompleted = false } return AnyView( HStack(spacing: 8) { ReminderToggle( isOn: Binding( get: { isCompleted }, set: { newValue in Task { await calendarManager.setReminderCompleted( reminderID: event.id, completed: newValue ) } } ), color: Color(event.calendar.color) ) .opacity(1.0) // Ensure the toggle is always fully opaque HStack { Text(event.title) .font(.callout) .foregroundColor(.white) .lineLimit(showFullEventTitles ? nil : 1) Spacer(minLength: 0) VStack(alignment: .trailing, spacing: 4) { if event.isAllDay { Text("All-day") .font(.caption) .fontWeight(.medium) .foregroundColor(.white) .lineLimit(1) } else { Text(event.start, style: .time) .foregroundColor(.white) .font(.caption) } } } .opacity( isCompleted ? 0.4 : event.start < Date.now && Calendar.current.isDateInToday(event.start) ? 0.6 : 1.0 ) } .padding(.vertical, 4) ) } else { return AnyView( HStack(alignment: .top, spacing: 4) { Rectangle() .fill(Color(event.calendar.color)) .frame(width: 3) .cornerRadius(1.5) VStack(alignment: .leading, spacing: 2) { Text(event.title) .font(.callout) .fontWeight(.medium) .foregroundColor(.white) .lineLimit(showFullEventTitles ? nil : 2) if let location = event.location, !location.isEmpty { Text(location) .font(.caption) .foregroundColor(Color(white: 0.65)) .lineLimit(1) } } Spacer(minLength: 0) VStack(alignment: .trailing, spacing: 4) { if event.isAllDay { Text("All-day") .font(.caption) .fontWeight(.medium) .foregroundColor(.white) .lineLimit(1) } else { Text(event.start, style: .time) .foregroundColor(.white) Text(event.end, style: .time) .foregroundColor(Color(white: 0.65)) } } .font(.caption) .frame(minWidth: 44, alignment: .trailing) } .opacity( event.eventStatus == .ended && Calendar.current.isDateInToday(event.start) ? 0.6 : 1.0) ) } } } struct ReminderToggle: View { @Binding var isOn: Bool var color: Color var body: some View { Button(action: { isOn.toggle() }) { ZStack { // Outer ring Circle() .strokeBorder(color, lineWidth: 2) .frame(width: 14, height: 14) // Inner fill if isOn { Circle() .fill(color) .frame(width: 8, height: 8) } Circle() .fill(Color.black.opacity(0.001)) .frame(width: 14, height: 14) } } .buttonStyle(PlainButtonStyle()) .padding(0) .accessibilityLabel(isOn ? "Mark as incomplete" : "Mark as complete") } } #Preview { CalendarView() .frame(width: 215, height: 130) .background(.black) .environmentObject(BoringViewModel()) } ================================================ FILE: boringNotch/components/EmptyState.swift ================================================ // // EmptyState.swift // // Created by Harsh Vardhan Goswami on 04/08/24. // import SwiftUI struct EmptyStateView: View { var message: String @State private var isVisible = true var body: some View { HStack { MinimalFaceFeatures( height: 70, width: 80) Text(message) .font(.system(size:14)) .foregroundColor(.gray) }.transition(.blurReplace.animation(.spring(.bouncy(duration: 0.3)))) // Smooth animation } } #Preview { EmptyStateView(message: "Play some music babies") } ================================================ FILE: boringNotch/components/HoverButton.swift ================================================ // // HoverButton.swift // boringNotch // // Created by Kraigo on 04.09.2024. // import SwiftUI struct HoverButton: View { var icon: String var iconColor: Color = .primary var scale: Image.Scale = .medium var action: () -> Void var contentTransition: ContentTransition = .symbolEffect; @State private var isHovering = false var body: some View { let size = CGFloat(scale == .large ? 40 : 30) Button(action: action) { Rectangle() .fill(.clear) .contentShape(Rectangle()) .frame(width: size, height: size) .overlay { Capsule() .fill(isHovering ? Color.gray.opacity(0.2) : .clear) .frame(width: size, height: size) .overlay { Image(systemName: icon) .foregroundColor(iconColor) .contentTransition(contentTransition) .font(scale == .large ? .largeTitle : .body) } } } .buttonStyle(PlainButtonStyle()) .onHover { hovering in withAnimation(.smooth(duration: 0.3)) { isHovering = hovering } } } } ================================================ FILE: boringNotch/components/Live activities/BoringBattery.swift ================================================ import SwiftUI import Defaults /// A view that displays the battery status with an icon and charging indicator. struct BatteryView: View { var levelBattery: Float var isPluggedIn: Bool var isCharging: Bool var isInLowPowerMode: Bool var batteryWidth: CGFloat = 26 var isForNotification: Bool var icon: String = "battery.0" /// Determines the icon to display when charging. var iconStatus: String { if isCharging { return "bolt" } else if isPluggedIn { return "plug" } else { return "" } } /// Determines the color of the battery based on its status. var batteryColor: Color { if isInLowPowerMode { return .yellow } else if levelBattery <= 20 && !isCharging && !isPluggedIn { return .red } else if isCharging || isPluggedIn || levelBattery == 100 { return .green } else { return .white } } var body: some View { ZStack(alignment: .leading) { Image(systemName: icon) .resizable() .fontWeight(.thin) .aspectRatio(contentMode: .fit) .foregroundColor(.white.opacity(0.5)) .frame( width: batteryWidth + 1 ) RoundedRectangle(cornerRadius: 2.5) .fill(batteryColor) .frame( width: CGFloat(((CGFloat(CFloat(levelBattery)) / 100) * (batteryWidth - 6))), height: (batteryWidth - 2.75) - 18 ) .padding(.leading, 2) if iconStatus != "" && (isForNotification || Defaults[.showPowerStatusIcons]) { ZStack { Image(iconStatus) .resizable() .aspectRatio(contentMode: .fit) .foregroundColor(.white) .frame( width: 17, height: 17 ) } .frame(width: batteryWidth, height: batteryWidth) } } } } struct ScaleButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .scaleEffect(configuration.isPressed ? 0.95 : 1.0) .animation(.easeInOut(duration: 0.2), value: configuration.isPressed) } } /// A view that displays detailed battery information and settings. struct BatteryMenuView: View { var isPluggedIn: Bool var isCharging: Bool var levelBattery: Float var maxCapacity: Float var timeToFullCharge: Int var isInLowPowerMode: Bool var onDismiss: () -> Void @Environment(\.openURL) private var openURL var body: some View { VStack(alignment: .leading, spacing: 16) { HStack { Text("Battery Status") .font(.headline) .fontWeight(.semibold) Spacer() Text("\(Int(levelBattery))%") .font(.headline) .fontWeight(.semibold) } VStack(alignment: .leading, spacing: 8) { Text("Max Capacity: \(Int(maxCapacity))%") .font(.subheadline) .fontWeight(.regular) if isInLowPowerMode { Label("Low Power Mode", systemImage: "bolt.circle") .font(.subheadline) .fontWeight(.regular) } if isCharging { Label("Charging", systemImage: "bolt.fill") .font(.subheadline) .fontWeight(.regular) } if isPluggedIn { Label("Plugged In", systemImage: "powerplug.fill") .font(.subheadline) .fontWeight(.regular) } if timeToFullCharge > 0 { Label("Time to Full Charge: \(timeToFullCharge) min", systemImage: "clock") .font(.subheadline) .fontWeight(.regular) } if !isCharging && isPluggedIn && levelBattery >= 80 { Label("Charging on Hold: Desktop Mode", systemImage: "desktopcomputer") .font(.subheadline) .fontWeight(.regular) } } .padding(.vertical, 8) Divider().background(Color.white) Button(action: openBatteryPreferences) { Label("Battery Settings", systemImage: "gearshape") .fontWeight(.regular) } .frame(maxWidth: .infinity) .buttonStyle(.plain) .padding(.vertical, 8) } .padding() .frame(width: 280) .foregroundColor(.white) } private func openBatteryPreferences() { if let url = URL(string: "x-apple.systempreferences:com.apple.preference.battery") { openURL(url) onDismiss() } } } /// A view that displays the battery status and allows interaction to show detailed information. struct BoringBatteryView: View { @State var batteryWidth: CGFloat = 26 var isCharging: Bool = false var isInLowPowerMode: Bool = false var isPluggedIn: Bool = false var levelBattery: Float = 0 var maxCapacity: Float = 0 var timeToFullCharge: Int = 0 @State var isForNotification: Bool = false @State private var showPopupMenu: Bool = false @State private var isPressed: Bool = false @State private var isHoveringButton: Bool = false @State private var isHoveringPopover: Bool = false @State private var hideTask: Task? = nil @EnvironmentObject var vm: BoringViewModel var body: some View { Button(action: { withAnimation { showPopupMenu.toggle() } }) { HStack { if Defaults[.showBatteryPercentage] { Text("\(Int32(levelBattery))%") .font(.callout) .foregroundStyle(.white) } BatteryView( levelBattery: levelBattery, isPluggedIn: isPluggedIn, isCharging: isCharging, isInLowPowerMode: isInLowPowerMode, batteryWidth: batteryWidth, isForNotification: isForNotification ) } } .buttonStyle(ScaleButtonStyle()) .popover( isPresented: $showPopupMenu, arrowEdge: .bottom) { BatteryMenuView( isPluggedIn: isPluggedIn, isCharging: isCharging, levelBattery: levelBattery, maxCapacity: maxCapacity, timeToFullCharge: timeToFullCharge, isInLowPowerMode: isInLowPowerMode, onDismiss: { showPopupMenu = false } ) .onHover { hovering in isHoveringPopover = hovering if hovering { hideTask?.cancel() hideTask = nil } else { scheduleHideIfNeeded() } } } .onChange(of: showPopupMenu) { vm.isBatteryPopoverActive = showPopupMenu } .onDisappear { hideTask?.cancel() hideTask = nil } } private func scheduleHideIfNeeded() { if isHoveringButton || isHoveringPopover { return } hideTask?.cancel() hideTask = Task { try? await Task.sleep(for: .milliseconds(350)) guard !Task.isCancelled else { return } await MainActor.run { withAnimation { showPopupMenu = false } } } } } #Preview { BoringBatteryView( batteryWidth: 30, isCharging: false, isInLowPowerMode: false, isPluggedIn: true, levelBattery: 80, maxCapacity: 100, timeToFullCharge: 10, isForNotification: false ).frame(width: 200, height: 200) } ================================================ FILE: boringNotch/components/Live activities/DownloadView.swift ================================================ // // DownloadView.swift // boringNotch // // Created by Harsh Vardhan Goswami on 17/08/24. // import Foundation import SwiftUI enum Browser { case safari case chrome } struct DownloadFile { var name: String var size: Int var formattedSize: String var browser: Browser } class DownloadWatcher: ObservableObject { @Published var downloadFiles: [DownloadFile] = [] } struct DownloadArea: View { @EnvironmentObject var watcher: DownloadWatcher var body: some View { HStack(alignment: .center) { HStack { if watcher.downloadFiles.first!.browser == .safari { AppIcon(for: "com.apple.safari") } else { Image(.chrome).resizable().scaledToFit().frame(width: 30, height: 30) } VStack(alignment: .leading) { Text("Download") Text("In progress").font(.system(.footnote)).foregroundStyle(.gray) } } Spacer() HStack(spacing: 12) { VStack(alignment: .trailing) { Text(watcher.downloadFiles.first!.formattedSize) Text(watcher.downloadFiles.first!.name).font(.caption2).foregroundStyle(.gray) } } } } } ================================================ FILE: boringNotch/components/Live activities/InlineHUD.swift ================================================ // // InlineHUDs.swift // boringNotch // // Created by Richard Kunkli on 14/09/2024. // import SwiftUI import Defaults struct InlineHUD: View { @EnvironmentObject var vm: BoringViewModel @Binding var type: SneakContentType @Binding var value: CGFloat @Binding var icon: String @Binding var hoverAnimation: Bool @Binding var gestureProgress: CGFloat var body: some View { HStack { HStack(spacing: 5) { Group { switch (type) { case .volume: if icon.isEmpty { Image(systemName: SpeakerSymbol(value)) .contentTransition(.interpolate) .symbolVariant(value > 0 ? .none : .slash) .frame(width: 20, height: 15, alignment: .leading) } else { Image(systemName: icon) .contentTransition(.interpolate) .opacity(value.isZero ? 0.6 : 1) .scaleEffect(value.isZero ? 0.85 : 1) .frame(width: 20, height: 15, alignment: .leading) } case .brightness: Image(systemName: BrightnessSymbol(value)) .contentTransition(.interpolate) .frame(width: 20, height: 15, alignment: .center) case .backlight: Image(systemName: value > 0.5 ? "light.max" : "light.min") .contentTransition(.interpolate) .frame(width: 20, height: 15, alignment: .center) case .mic: Image(systemName: "mic") .symbolRenderingMode(.hierarchical) .symbolVariant(value > 0 ? .none : .slash) .contentTransition(.interpolate) .frame(width: 20, height: 15, alignment: .center) default: EmptyView() } } .foregroundStyle(.white) .symbolVariant(.fill) Text(Type2Name(type)) .font(.subheadline) .fontWeight(.medium) .lineLimit(1) .allowsTightening(true) .contentTransition(.numericText()) } .frame(width: 100 - (hoverAnimation ? 0 : 12) + gestureProgress / 2, height: vm.notchSize.height - (hoverAnimation ? 0 : 12), alignment: .leading) Rectangle() .fill(.black) .frame(width: vm.closedNotchSize.width - 20) HStack { if (type == .mic) { Text(value.isZero ? "muted" : "unmuted") .foregroundStyle(.gray) .lineLimit(1) .allowsTightening(true) .multilineTextAlignment(.trailing) .frame(maxWidth: .infinity, alignment: .trailing) .contentTransition(.interpolate) } else { HStack { DraggableProgressBar(value: $value, onChange: { v in if type == .volume { VolumeManager.shared.setAbsolute(Float32(v)) } else if type == .brightness { BrightnessManager.shared.setAbsolute(value: Float32(v)) } }) if (type == .volume && value.isZero) { Text("muted") .font(.caption) .fontWeight(.medium) .foregroundStyle(.gray) .lineLimit(1) .allowsTightening(true) .multilineTextAlignment(.trailing) } else if Defaults[.showClosedNotchHUDPercentage] { Text("\(Int(value * 100))%") .font(.caption) .fontWeight(.medium) .foregroundStyle(.gray) .lineLimit(1) .allowsTightening(true) .multilineTextAlignment(.trailing) } } } } .padding(.trailing, 4) .frame(width: 100 - (hoverAnimation ? 0 : 12) + gestureProgress / 2, height: vm.closedNotchSize.height - (hoverAnimation ? 0 : 12), alignment: .center) } .frame(height: vm.closedNotchSize.height + (hoverAnimation ? 8 : 0), alignment: .center) } func SpeakerSymbol(_ value: CGFloat) -> String { switch(value) { case 0: return "speaker" case 0...0.3: return "speaker.wave.1" case 0.3...0.8: return "speaker.wave.2" case 0.8...1: return "speaker.wave.3" default: return "speaker.wave.2" } } func BrightnessSymbol(_ value: CGFloat) -> String { switch(value) { case 0...0.6: return "sun.min" case 0.6...1: return "sun.max" default: return "sun.min" } } func Type2Name(_ type: SneakContentType) -> String { switch(type) { case .volume: return "Volume" case .brightness: return "Brightness" case .backlight: return "Backlight" case .mic: return "Mic" default: return "" } } } #Preview { InlineHUD(type: .constant(.brightness), value: .constant(0.4), icon: .constant(""), hoverAnimation: .constant(false), gestureProgress: .constant(0)) .padding(.horizontal, 8) .background(Color.black) .padding() .environmentObject(BoringViewModel()) } ================================================ FILE: boringNotch/components/Live activities/LiveActivityModifier.swift ================================================ // // LiveActivityModifier.swift // boringNotch // // Created by Richard Kunkli on 12/08/2024. // import SwiftUI enum ActivityType { case mediaPlayback case charging case download } struct LiveActivityModifier: ViewModifier { let `for`: ActivityType let leftContent: () -> Left let rightContent: () -> Right func body(content: Content) -> some View { content .overlay( HStack { leftContent() Spacer() //.frame(minWidth: vm.closedNotchSize.width) rightContent() } .padding() ) } } extension View { func liveActivity( for activityId: ActivityType, @ViewBuilder left: @escaping () -> Left, @ViewBuilder right: @escaping () -> Right ) -> some View { self.modifier(LiveActivityModifier(for: activityId, leftContent: left, rightContent: right)) } } ================================================ FILE: boringNotch/components/Live activities/MarqueeTextView.swift ================================================ // // MarqueeTextView.swift // boringNotch // // Created by Richard Kunkli on 08/08/2024. // import SwiftUI struct SizePreferenceKey: PreferenceKey { static var defaultValue: CGSize = .zero static func reduce(value: inout CGSize, nextValue: () -> CGSize) { value = nextValue() } } struct MeasureSizeModifier: ViewModifier { func body(content: Content) -> some View { content.background(GeometryReader { geometry in Color.clear.preference(key: SizePreferenceKey.self, value: geometry.size) }) } } struct MarqueeText: View { @Binding var text: String let font: Font let nsFont: NSFont.TextStyle let textColor: Color let backgroundColor: Color let minDuration: Double let frameWidth: CGFloat @State private var animate = false @State private var textSize: CGSize = .zero @State private var offset: CGFloat = 0 init(_ text: Binding, font: Font = .body, nsFont: NSFont.TextStyle = .body, textColor: Color = .primary, backgroundColor: Color = .clear, minDuration: Double = 3.0, frameWidth: CGFloat = 200) { _text = text self.font = font self.nsFont = nsFont self.textColor = textColor self.backgroundColor = backgroundColor self.minDuration = minDuration self.frameWidth = frameWidth } private var needsScrolling: Bool { textSize.width > frameWidth } var body: some View { GeometryReader { geometry in ZStack(alignment: .leading) { HStack(spacing: 20) { Text(text) Text(text) .opacity(needsScrolling ? 1 : 0) } .id(text) .font(font) .foregroundColor(textColor) .fixedSize(horizontal: true, vertical: false) .offset(x: self.animate ? offset : 0) .animation( self.animate ? .linear(duration: Double(textSize.width / 30)) .delay(minDuration) .repeatForever(autoreverses: false) : .none, value: self.animate ) .background(backgroundColor) .modifier(MeasureSizeModifier()) .onPreferenceChange(SizePreferenceKey.self) { size in self.textSize = CGSize(width: size.width / 2, height: NSFont.preferredFont(forTextStyle: nsFont).pointSize) self.animate = false self.offset = 0 DispatchQueue.main.asyncAfter(deadline: .now() + 0.01){ if needsScrolling { self.animate = true self.offset = -(textSize.width + 10) } } } } .frame(width: frameWidth, alignment: .leading) .clipped() } .frame(height: textSize.height * 1.3) } } ================================================ FILE: boringNotch/components/Live activities/OpenNotchHUD.swift ================================================ // // OpenNotchHUD.swift // boringNotch // // Created by Alexander on 2024-11-23. // import SwiftUI import Defaults struct OpenNotchHUD: View { @EnvironmentObject var vm: BoringViewModel @Binding var type: SneakContentType @Binding var value: CGFloat @Binding var icon: String @Default(.showOpenNotchHUDPercentage) var showPercentage var body: some View { HStack(spacing: 8) { // Icon Group { switch type { case .volume: if icon.isEmpty { Image(systemName: SpeakerSymbol(value)) .contentTransition(.interpolate) } else { Image(systemName: icon) .contentTransition(.interpolate) } case .brightness: Image(systemName: "sun.max.fill") .contentTransition(.symbolEffect) case .backlight: Image(systemName: value > 0.5 ? "light.max" : "light.min") .contentTransition(.interpolate) case .mic: Image(systemName: "mic") .symbolVariant(value > 0 ? .none : .slash) .contentTransition(.interpolate) default: EmptyView() } } .font(.system(size: 14, weight: .medium)) .foregroundStyle(.white) .frame(width: 20, alignment: .center) // Slider or Status Text if type != .mic { DraggableProgressBar(value: $value, onChange: { newVal in updateSystemValue(newVal) }) .frame(width: showPercentage ? 65 : 108) // Fixed width for consistency } else { Text(value > 0 ? "Unmuted" : "Muted") .font(.system(size: 13, weight: .medium)) .foregroundStyle(.white) .fixedSize() } // Percentage Text if type != .mic && showPercentage { Text("\(Int(value * 100))%") .font(.system(size: 12, weight: .medium)) .foregroundStyle(.gray) .monospacedDigit() .frame(width: 35, alignment: .trailing) } } .padding(.horizontal, 10) .padding(.vertical, 6) .background( Capsule() .fill(Color.black) .stroke(Color.white.opacity(0.1), lineWidth: 1) ) } func SpeakerSymbol(_ value: CGFloat) -> String { switch(value) { case 0: return "speaker.slash" case 0...0.33: return "speaker.wave.1" case 0.33...0.66: return "speaker.wave.2" default: return "speaker.wave.3" } } func updateSystemValue(_ newVal: CGFloat) { switch type { case .volume: VolumeManager.shared.setAbsolute(Float32(newVal)) case .brightness: BrightnessManager.shared.setAbsolute(value: Float32(newVal)) default: break } } } #Preview { OpenNotchHUD(type: .constant(.volume), value: .constant(0.5), icon: .constant("")) .environmentObject(BoringViewModel()) .padding() .background(Color.gray) } ================================================ FILE: boringNotch/components/Live activities/SystemEventIndicatorModifier.swift ================================================ // // SystemEventIndicatorModifier.swift // boringNotch // // Created by Richard Kunkli on 12/08/2024. // import SwiftUI import Defaults struct SystemEventIndicatorModifier: View { @EnvironmentObject var vm: BoringViewModel @Binding var eventType: SneakContentType @Binding var value: CGFloat { didSet { DispatchQueue.main.async { self.sendEventBack(value) self.vm.objectWillChange.send() } } } @Binding var icon: String let showSlider: Bool = false var sendEventBack: (CGFloat) -> Void var body: some View { HStack(spacing: 14) { switch (eventType) { case .volume: if icon.isEmpty { Image(systemName: SpeakerSymbol(value)) .contentTransition(.interpolate) .symbolVariant(value > 0 ? .none : .slash) .frame(width: 20, height: 15, alignment: .leading) } else { Image(systemName: icon) .contentTransition(.interpolate) .opacity(value.isZero ? 0.6 : 1) .scaleEffect(value.isZero ? 0.85 : 1) .frame(width: 20, height: 15, alignment: .leading) } case .brightness: Image(systemName: "sun.max.fill") .contentTransition(.symbolEffect) .frame(width: 20, height: 15) .foregroundStyle(.white) case .backlight: Image(systemName: value > 0.5 ? "light.max" : "light.min") .contentTransition(.interpolate) .frame(width: 20, height: 15) .foregroundStyle(.white) case .mic: Image(systemName: "mic") .symbolVariant(value > 0 ? .none : .slash) .contentTransition(.interpolate) .frame(width: 20, height: 15) .foregroundStyle(.white) default: EmptyView() } if (eventType != .mic) { DraggableProgressBar(value: $value) if Defaults[.showClosedNotchHUDPercentage] { Text("\(Int(value * 100))%") .font(.system(size: 12, weight: .medium)) .foregroundStyle(.white) .monospacedDigit() .frame(width: 35, alignment: .trailing) } } else { Text("Mic \(value > 0 ? "unmuted" : "muted")") .foregroundStyle(.gray) .lineLimit(1) .allowsTightening(true) } } .frame(maxWidth: .infinity, alignment: .leading) .symbolVariant(.fill) .imageScale(.large) } func SpeakerSymbol(_ value: CGFloat) -> String { switch(value) { case 0: return "speaker.slash" case 0...0.3: return "speaker.wave.1" case 0.3...0.8: return "speaker.wave.2" case 0.8...1: return "speaker.wave.3" default: return "speaker.wave.2" } } } struct DraggableProgressBar: View { @EnvironmentObject var vm: BoringViewModel @Binding var value: CGFloat var onChange: ((CGFloat) -> Void)? = nil @State private var isDragging = false @State private var dragOffset: CGFloat = 0 var body: some View { VStack { GeometryReader { geo in ZStack(alignment: .leading) { Capsule() .fill(.tertiary) Capsule() .fill( Defaults[.enableGradient] ? AnyShapeStyle(LinearGradient( colors: Defaults[.systemEventIndicatorUseAccent] ? [Color.effectiveAccent, Color.effectiveAccent.ensureMinimumBrightness(factor: 0.2)] : [Color.white, Color.white.opacity(0.2)], startPoint: .trailing, endPoint: .leading )) : AnyShapeStyle(Defaults[.systemEventIndicatorUseAccent] ? Color.effectiveAccent : Color.white) ) .frame(width: max(0, min(geo.size.width * value, geo.size.width))) .shadow(color: Defaults[.systemEventIndicatorShadow] ? (Defaults[.systemEventIndicatorUseAccent] ? Color.effectiveAccent.ensureMinimumBrightness(factor: 0.7) : Color.white) : Color.clear, radius: 8, x: 3) .opacity(value.isZero ? 0 : 1) } .gesture( DragGesture(minimumDistance: 0) .onChanged { gesture in withAnimation(.smooth(duration: 0.3)) { isDragging = true updateValue(gesture: gesture, in: geo) } } .onEnded { _ in withAnimation(.smooth(duration: 0.3)) { isDragging = false } } ) } .frame(height: Defaults[.inlineHUD] ? isDragging ? 8 : 5 : isDragging ? 9 : 6) } } private func updateValue(gesture: DragGesture.Value, in geometry: GeometryProxy) { let dragPosition = gesture.location.x let newValue = dragPosition / geometry.size.width value = max(0, min(newValue, 1)) onChange?(value) } } ================================================ FILE: boringNotch/components/LottieView.swift ================================================ // // LottieView.swift // boringNotch // // Created by Alexander on 2025-11-14. // import SwiftUI import Lottie import ObjectiveC struct LottieView: NSViewRepresentable { let url: URL let speed: Double let loopMode: LottieLoopMode private static var associatedURLKey: UInt8 = 0 func makeNSView(context: Context) -> NSView { let animationView = LottieAnimationView() animationView.translatesAutoresizingMaskIntoConstraints = false let container = NSView() container.addSubview(animationView) NSLayoutConstraint.activate([ animationView.leadingAnchor.constraint(equalTo: container.leadingAnchor), animationView.trailingAnchor.constraint(equalTo: container.trailingAnchor), animationView.topAnchor.constraint(equalTo: container.topAnchor), animationView.bottomAnchor.constraint(equalTo: container.bottomAnchor) ]) return container } func updateNSView(_ nsView: NSView, context: Context) { guard let animationView = nsView.subviews.first as? LottieAnimationView else { return } let lastURL = objc_getAssociatedObject(animationView, &Self.associatedURLKey) as? URL if lastURL != url { LottieAnimation.loadedFrom(url: url) { animation in animationView.animation = animation animationView.loopMode = loopMode animationView.animationSpeed = CGFloat(speed) animationView.play() objc_setAssociatedObject(animationView, &Self.associatedURLKey, url, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } else { animationView.loopMode = loopMode animationView.animationSpeed = CGFloat(speed) if !animationView.isAnimationPlaying { animationView.play() } } } } ================================================ FILE: boringNotch/components/Music/LottieAnimationView.swift ================================================ // // LottieAnimationContainer.swift // boringNotch // // Created by Richard Kunkli on 2024. 10. 29.. // import SwiftUI import Defaults struct LottieAnimationContainer: View { @Default(.selectedVisualizer) var selectedVisualizer var body: some View { if selectedVisualizer == nil { LottieView(url: URL(string: "https://assets9.lottiefiles.com/packages/lf20_mniampqn.json")!, speed: 1.0, loopMode: .loop) } else { LottieView(url: selectedVisualizer!.url, speed: selectedVisualizer!.speed, loopMode: .loop) } } } #Preview { LottieAnimationContainer() } ================================================ FILE: boringNotch/components/Music/MusicVisualizer.swift ================================================ // // MusicVisualizer.swift // boringNotch // // Created by Harsh Vardhan Goswami on 02/08/24. // import AppKit import Cocoa import SwiftUI class AudioSpectrum: NSView { private var barLayers: [CAShapeLayer] = [] private var barScales: [CGFloat] = [] private var isPlaying: Bool = true private var animationTimer: Timer? override init(frame frameRect: NSRect) { super.init(frame: frameRect) wantsLayer = true setupBars() } required init?(coder: NSCoder) { super.init(coder: coder) wantsLayer = true setupBars() } private func setupBars() { let barWidth: CGFloat = 2 let barCount = 4 let spacing: CGFloat = barWidth let totalWidth = CGFloat(barCount) * (barWidth + spacing) let totalHeight: CGFloat = 14 frame.size = CGSize(width: totalWidth, height: totalHeight) for i in 0 ..< barCount { let xPosition = CGFloat(i) * (barWidth + spacing) let barLayer = CAShapeLayer() barLayer.frame = CGRect(x: xPosition, y: 0, width: barWidth, height: totalHeight) barLayer.anchorPoint = CGPoint(x: 0.5, y: 0.5) barLayer.position = CGPoint(x: xPosition + barWidth / 2, y: totalHeight / 2) barLayer.fillColor = NSColor.white.cgColor barLayer.backgroundColor = NSColor.white.cgColor barLayer.allowsGroupOpacity = false barLayer.masksToBounds = true let path = NSBezierPath(roundedRect: CGRect(x: 0, y: 0, width: barWidth, height: totalHeight), xRadius: barWidth / 2, yRadius: barWidth / 2) barLayer.path = path.cgPath barLayers.append(barLayer) barScales.append(0.35) layer?.addSublayer(barLayer) } } private func startAnimating() { guard animationTimer == nil else { return } animationTimer = Timer.scheduledTimer(withTimeInterval: 0.3, repeats: true) { [weak self] _ in self?.updateBars() } } private func stopAnimating() { animationTimer?.invalidate() animationTimer = nil resetBars() } private func updateBars() { for (i, barLayer) in barLayers.enumerated() { let currentScale = barScales[i] let targetScale = CGFloat.random(in: 0.35 ... 1.0) barScales[i] = targetScale let animation = CABasicAnimation(keyPath: "transform.scale.y") animation.fromValue = currentScale animation.toValue = targetScale animation.duration = 0.3 animation.autoreverses = true animation.fillMode = .forwards animation.isRemovedOnCompletion = false if #available(macOS 13.0, *) { animation.preferredFrameRateRange = CAFrameRateRange(minimum: 24, maximum: 24, preferred: 24) } barLayer.add(animation, forKey: "scaleY") } } private func resetBars() { for (i, barLayer) in barLayers.enumerated() { barLayer.removeAllAnimations() barLayer.transform = CATransform3DMakeScale(1, 0.35, 1) barScales[i] = 0.35 } } func setPlaying(_ playing: Bool) { isPlaying = playing if isPlaying { startAnimating() } else { stopAnimating() } } } struct AudioSpectrumView: NSViewRepresentable { @Binding var isPlaying: Bool func makeNSView(context: Context) -> AudioSpectrum { let spectrum = AudioSpectrum() spectrum.setPlaying(isPlaying) return spectrum } func updateNSView(_ nsView: AudioSpectrum, context: Context) { nsView.setPlaying(isPlaying) } } #Preview { AudioSpectrumView(isPlaying: .constant(true)) .frame(width: 16, height: 20) .padding() } ================================================ FILE: boringNotch/components/Notch/BoringExtrasMenu.swift ================================================ // // BoringExtrasMenu.swift // boringNotch // // Created by Harsh Vardhan Goswami on 04/08/24. // import SwiftUI struct BoringLargeButtons: View { var action: () -> Void var icon: Image var title: String var body: some View { Button ( action:action, label: { ZStack { RoundedRectangle(cornerRadius: 12.0).fill(.black).frame(width: 70, height: 70) VStack(spacing: 8) { icon.resizable() .aspectRatio(contentMode: .fit).frame(width:20) Text(title).font(.body) } } }).buttonStyle(PlainButtonStyle()).shadow(color: .black.opacity(0.5), radius: 10) } } struct BoringExtrasMenu : View { @ObservedObject var vm: BoringViewModel var body: some View { VStack{ HStack(spacing: 20) { hide settings close } } } var github: some View { BoringLargeButtons( action: { if let url = URL(string: "https://github.com/TheBoredTeam/boring.notch") { NSWorkspace.shared.open(url) } }, icon: Image(.github), title: "Checkout" ) } var settings: some View { Button(action: { SettingsWindowController.shared.showWindow() }) { ZStack { RoundedRectangle(cornerRadius: 12.0).fill(.black).frame(width: 70, height: 70) VStack(spacing: 8) { Image(systemName: "gear").resizable() .aspectRatio(contentMode: .fit).frame(width:20) Text("Settings").font(.body) } } } .buttonStyle(PlainButtonStyle()).shadow(color: .black.opacity(0.5), radius: 10) } var hide: some View { BoringLargeButtons( action: { DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { //vm.openMusic() } }, icon: Image(systemName: "arrow.down.forward.and.arrow.up.backward"), title: "Hide" ) } var close: some View { BoringLargeButtons( action: { DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { NSApp.terminate(nil) } } }, icon: Image(systemName: "xmark"), title: "Exit" ) } } #Preview { BoringExtrasMenu(vm: .init()) } ================================================ FILE: boringNotch/components/Notch/BoringHeader.swift ================================================ // // BoringHeader.swift // boringNotch // // Created by Harsh Vardhan Goswami on 04/08/24. // import Defaults import SwiftUI struct BoringHeader: View { @EnvironmentObject var vm: BoringViewModel @ObservedObject var batteryModel = BatteryStatusViewModel.shared @ObservedObject var coordinator = BoringViewCoordinator.shared @StateObject var tvm = ShelfStateViewModel.shared var body: some View { HStack(spacing: 0) { HStack { if (!tvm.isEmpty || coordinator.alwaysShowTabs) && Defaults[.boringShelf] { TabSelectionView() } else if vm.notchState == .open { EmptyView() } } .frame(maxWidth: .infinity, alignment: .leading) .opacity(vm.notchState == .closed ? 0 : 1) .blur(radius: vm.notchState == .closed ? 20 : 0) .zIndex(2) if vm.notchState == .open { Rectangle() .fill(NSScreen.screen(withUUID: coordinator.selectedScreenUUID)?.safeAreaInsets.top ?? 0 > 0 ? .black : .clear) .frame(width: vm.closedNotchSize.width) .mask { NotchShape() } } HStack(spacing: 4) { if vm.notchState == .open { if isHUDType(coordinator.sneakPeek.type) && coordinator.sneakPeek.show && Defaults[.showOpenNotchHUD] { OpenNotchHUD(type: $coordinator.sneakPeek.type, value: $coordinator.sneakPeek.value, icon: $coordinator.sneakPeek.icon) .transition(.scale(scale: 0.8).combined(with: .opacity)) } else { if Defaults[.showMirror] { Button(action: { vm.toggleCameraPreview() }) { Capsule() .fill(.black) .frame(width: 30, height: 30) .overlay { Image(systemName: "web.camera") .foregroundColor(.white) .padding() .imageScale(.medium) } } .buttonStyle(PlainButtonStyle()) } if Defaults[.settingsIconInNotch] { Button(action: { SettingsWindowController.shared.showWindow() }) { Capsule() .fill(.black) .frame(width: 30, height: 30) .overlay { Image(systemName: "gear") .foregroundColor(.white) .padding() .imageScale(.medium) } } .buttonStyle(PlainButtonStyle()) } if Defaults[.showBatteryIndicator] { BoringBatteryView( batteryWidth: 30, isCharging: batteryModel.isCharging, isInLowPowerMode: batteryModel.isInLowPowerMode, isPluggedIn: batteryModel.isPluggedIn, levelBattery: batteryModel.levelBattery, maxCapacity: batteryModel.maxCapacity, timeToFullCharge: batteryModel.timeToFullCharge, isForNotification: false ) } } } } .font(.system(.headline, design: .rounded)) .frame(maxWidth: .infinity, alignment: .trailing) .opacity(vm.notchState == .closed ? 0 : 1) .blur(radius: vm.notchState == .closed ? 20 : 0) .zIndex(2) } .foregroundColor(.gray) .environmentObject(vm) } func isHUDType(_ type: SneakContentType) -> Bool { switch type { case .volume, .brightness, .backlight, .mic: return true default: return false } } } #Preview { BoringHeader().environmentObject(BoringViewModel()) } ================================================ FILE: boringNotch/components/Notch/BoringNotchSkyLightWindow.swift ================================================ // // BoringNotchSkyLightWindow.swift // boringNotch // // Created by Alexander on 2025-10-20. // import Cocoa import SkyLightWindow import Defaults import Combine extension SkyLightOperator { func undelegateWindow(_ window: NSWindow) { typealias F_SLSRemoveWindowsFromSpaces = @convention(c) (Int32, CFArray, CFArray) -> Int32 let handler = dlopen("/System/Library/PrivateFrameworks/SkyLight.framework/Versions/A/SkyLight", RTLD_NOW) guard let SLSRemoveWindowsFromSpaces = unsafeBitCast( dlsym(handler, "SLSRemoveWindowsFromSpaces"), to: F_SLSRemoveWindowsFromSpaces?.self ) else { return } // Remove the window from the SkyLight space _ = SLSRemoveWindowsFromSpaces( connection, [window.windowNumber] as CFArray, [space] as CFArray ) } } class BoringNotchSkyLightWindow: NSPanel { private var isSkyLightEnabled: Bool = false override init( contentRect: NSRect, styleMask: NSWindow.StyleMask, backing: NSWindow.BackingStoreType, defer flag: Bool ) { super.init( contentRect: contentRect, styleMask: styleMask, backing: backing, defer: flag ) configureWindow() setupObservers() } private func configureWindow() { isFloatingPanel = true isOpaque = false titleVisibility = .hidden titlebarAppearsTransparent = true backgroundColor = .clear isMovable = false level = .mainMenu + 3 hasShadow = false isReleasedWhenClosed = false // Force dark appearance regardless of system setting appearance = NSAppearance(named: .darkAqua) collectionBehavior = [ .fullScreenAuxiliary, .stationary, .canJoinAllSpaces, .ignoresCycle, ] // Apply initial sharing type setting updateSharingType() } private func setupObservers() { // Listen for changes to the hideFromScreenRecording setting Defaults.publisher(.hideFromScreenRecording) .sink { [weak self] _ in self?.updateSharingType() } .store(in: &observers) } private func updateSharingType() { if Defaults[.hideFromScreenRecording] { sharingType = .none } else { sharingType = .readWrite } } func enableSkyLight() { if !isSkyLightEnabled { SkyLightOperator.shared.delegateWindow(self) isSkyLightEnabled = true } } func disableSkyLight() { if isSkyLightEnabled { SkyLightOperator.shared.undelegateWindow(self) isSkyLightEnabled = false } } private var observers: Set = [] override var canBecomeKey: Bool { false } override var canBecomeMain: Bool { false } } ================================================ FILE: boringNotch/components/Notch/BoringNotchWindow.swift ================================================ // // BoringNotchWindow.swift // boringNotch // // Created by Harsh Vardhan Goswami on 06/08/24. // import Cocoa class BoringNotchWindow: NSPanel { override init( contentRect: NSRect, styleMask: NSWindow.StyleMask, backing: NSWindow.BackingStoreType, defer flag: Bool ) { super.init( contentRect: contentRect, styleMask: styleMask, backing: backing, defer: flag ) isFloatingPanel = true isOpaque = false titleVisibility = .hidden titlebarAppearsTransparent = true backgroundColor = .clear isMovable = false collectionBehavior = [ .fullScreenAuxiliary, .stationary, .canJoinAllSpaces, .ignoresCycle, ] isReleasedWhenClosed = false level = .mainMenu + 3 hasShadow = false } override var canBecomeKey: Bool { false } override var canBecomeMain: Bool { false } } ================================================ FILE: boringNotch/components/Notch/NotchHomeView.swift ================================================ // // NotchHomeView.swift // boringNotch // // Created by Hugo Persson on 2024-08-18. // Modified by Harsh Vardhan Goswami & Richard Kunkli & Mustafa Ramadan // import Combine import Defaults import SwiftUI // MARK: - Music Player Components struct MusicPlayerView: View { @EnvironmentObject var vm: BoringViewModel let albumArtNamespace: Namespace.ID var body: some View { HStack { AlbumArtView(vm: vm, albumArtNamespace: albumArtNamespace).padding(.all, 5) MusicControlsView().drawingGroup().compositingGroup() } } } struct AlbumArtView: View { @ObservedObject var musicManager = MusicManager.shared @ObservedObject var vm: BoringViewModel let albumArtNamespace: Namespace.ID var body: some View { ZStack(alignment: .bottomTrailing) { if Defaults[.lightingEffect] { albumArtBackground } albumArtButton } } private var albumArtBackground: some View { Image(nsImage: musicManager.albumArt) .resizable() .clipped() .clipShape( RoundedRectangle( cornerRadius: Defaults[.cornerRadiusScaling] ? MusicPlayerImageSizes.cornerRadiusInset.opened : MusicPlayerImageSizes.cornerRadiusInset.closed) ) .aspectRatio(1, contentMode: .fit) .scaleEffect(x: 1.3, y: 1.4) .rotationEffect(.degrees(92)) .blur(radius: 40) .opacity(musicManager.isPlaying ? 0.5 : 0) } private var albumArtButton: some View { ZStack { Button { musicManager.openMusicApp() } label: { ZStack(alignment:.bottomTrailing) { albumArtImage appIconOverlay } } .buttonStyle(PlainButtonStyle()) .scaleEffect(musicManager.isPlaying ? 1 : 0.85) albumArtDarkOverlay } } private var albumArtDarkOverlay: some View { Rectangle() .aspectRatio(1, contentMode: .fit) .foregroundColor(Color.black) .opacity(musicManager.isPlaying ? 0 : 0.8) .blur(radius: 50) } private var albumArtImage: some View { Image(nsImage: musicManager.albumArt) .resizable() .aspectRatio(1, contentMode: .fit) .matchedGeometryEffect(id: "albumArt", in: albumArtNamespace) .clipped() .clipShape( RoundedRectangle( cornerRadius: Defaults[.cornerRadiusScaling] ? MusicPlayerImageSizes.cornerRadiusInset.opened : MusicPlayerImageSizes.cornerRadiusInset.closed) ) } @ViewBuilder private var appIconOverlay: some View { if vm.notchState == .open && !musicManager.usingAppIconForArtwork { AppIcon(for: musicManager.bundleIdentifier ?? "com.apple.Music") .resizable() .aspectRatio(contentMode: .fill) .frame(width: 30, height: 30) .offset(x: 10, y: 10) .transition(.scale.combined(with: .opacity)) .zIndex(2) } } } struct MusicControlsView: View { @ObservedObject var musicManager = MusicManager.shared @EnvironmentObject var vm: BoringViewModel @ObservedObject var webcamManager = WebcamManager.shared @State private var sliderValue: Double = 0 @State private var dragging: Bool = false @State private var lastDragged: Date = .distantPast @Default(.musicControlSlots) private var slotConfig @Default(.musicControlSlotLimit) private var slotLimit var body: some View { VStack(alignment: .leading) { songInfoAndSlider slotToolbar } .buttonStyle(PlainButtonStyle()) } private var songInfoAndSlider: some View { GeometryReader { geo in VStack(alignment: .leading, spacing: 4) { songInfo(width: geo.size.width) musicSlider } } .padding(.top, 10) .padding(.leading, 5) } private func songInfo(width: CGFloat) -> some View { VStack(alignment: .leading, spacing: 0) { MarqueeText( $musicManager.songTitle, font: .headline, nsFont: .headline, textColor: .white, frameWidth: width) MarqueeText( $musicManager.artistName, font: .headline, nsFont: .headline, textColor: Defaults[.playerColorTinting] ? Color(nsColor: musicManager.avgColor) .ensureMinimumBrightness(factor: 0.6) : .gray, frameWidth: width ) .fontWeight(.medium) if Defaults[.enableLyrics] { TimelineView(.animation(minimumInterval: 0.25)) { timeline in let currentElapsed: Double = { guard musicManager.isPlaying else { return musicManager.elapsedTime } let delta = timeline.date.timeIntervalSince(musicManager.timestampDate) let progressed = musicManager.elapsedTime + (delta * musicManager.playbackRate) return min(max(progressed, 0), musicManager.songDuration) }() let line: String = { if musicManager.isFetchingLyrics { return "Loading lyrics…" } if !musicManager.syncedLyrics.isEmpty { return musicManager.lyricLine(at: currentElapsed) } let trimmed = musicManager.currentLyrics.trimmingCharacters(in: .whitespacesAndNewlines) return trimmed.isEmpty ? "No lyrics found" : trimmed.replacingOccurrences(of: "\n", with: " ") }() let isPersian = line.unicodeScalars.contains { scalar in let v = scalar.value return v >= 0x0600 && v <= 0x06FF } MarqueeText( .constant(line), font: .subheadline, nsFont: .subheadline, textColor: musicManager.isFetchingLyrics ? .gray.opacity(0.7) : .gray, frameWidth: width ) .font(isPersian ? .custom("Vazirmatn-Regular", size: NSFont.preferredFont(forTextStyle: .subheadline).pointSize) : .subheadline) .lineLimit(1) .opacity(musicManager.isPlaying ? 1 : 0) .transition(.opacity.combined(with: .move(edge: .top))) } } } } private var musicSlider: some View { TimelineView(.animation(minimumInterval: musicManager.playbackRate > 0 ? 0.1 : nil)) { timeline in MusicSliderView( sliderValue: $sliderValue, duration: $musicManager.songDuration, lastDragged: $lastDragged, color: musicManager.avgColor, dragging: $dragging, currentDate: timeline.date, timestampDate: musicManager.timestampDate, elapsedTime: musicManager.elapsedTime, playbackRate: musicManager.playbackRate, isPlaying: musicManager.isPlaying ) { newValue in MusicManager.shared.seek(to: newValue) } .padding(.top, 5) .frame(height: 36) } } private var slotToolbar: some View { let slots = activeSlots return HStack(spacing: 6) { ForEach(Array(slots.enumerated()), id: \.offset) { index, slot in slotView(for: slot) .frame(alignment: .center) } } .frame(maxWidth: .infinity, alignment: .center) } private var activeSlots: [MusicControlButton] { let sanitizedLimit = min( max(slotLimit, MusicControlButton.minSlotCount), MusicControlButton.maxSlotCount ) let padded = slotConfig.padded(to: sanitizedLimit, filler: .none) let result = Array(padded.prefix(sanitizedLimit)) // If calendar and camera are both visible alongside music, hide the edge slots let shouldHideEdges = Defaults[.showCalendar] && Defaults[.showMirror] && webcamManager.cameraAvailable && vm.isCameraExpanded if shouldHideEdges && result.count >= 5 { return Array(result.dropFirst().dropLast()) } return result } @ViewBuilder private func slotView(for slot: MusicControlButton) -> some View { switch slot { case .shuffle: HoverButton(icon: "shuffle", iconColor: musicManager.isShuffled ? .red : .primary, scale: .medium) { MusicManager.shared.toggleShuffle() } case .previous: HoverButton(icon: "backward.fill", scale: .medium) { MusicManager.shared.previousTrack() } case .playPause: HoverButton(icon: musicManager.isPlaying ? "pause.fill" : "play.fill", scale: .large) { MusicManager.shared.togglePlay() } case .next: HoverButton(icon: "forward.fill", scale: .medium) { MusicManager.shared.nextTrack() } case .repeatMode: HoverButton(icon: repeatIcon, iconColor: repeatIconColor, scale: .medium) { MusicManager.shared.toggleRepeat() } case .volume: VolumeControlView() case .favorite: FavoriteControlButton() case .goBackward: HoverButton(icon: "gobackward.15", scale: .medium) { MusicManager.shared.skip(seconds: -15) } case .goForward: HoverButton(icon: "goforward.15", scale: .medium) { MusicManager.shared.skip(seconds: 15) } case .none: Color.clear.frame(height: 1) } } private var repeatIcon: String { switch musicManager.repeatMode { case .off: return "repeat" case .all: return "repeat" case .one: return "repeat.1" } } private var repeatIconColor: Color { switch musicManager.repeatMode { case .off: return .primary case .all, .one: return .red } } } struct FavoriteControlButton: View { @ObservedObject var musicManager = MusicManager.shared var body: some View { HoverButton(icon: iconName, iconColor: iconColor, scale: .medium) { MusicManager.shared.toggleFavoriteTrack() } .disabled(!musicManager.canFavoriteTrack) .opacity(musicManager.canFavoriteTrack ? 1 : 0.35) } private var iconName: String { musicManager.isFavoriteTrack ? "heart.fill" : "heart" } private var iconColor: Color { musicManager.isFavoriteTrack ? .red : .primary } } private extension Array where Element == MusicControlButton { func padded(to length: Int, filler: MusicControlButton) -> [MusicControlButton] { if count >= length { return self } return self + Array(repeating: filler, count: length - count) } } // MARK: - Volume Control View struct VolumeControlView: View { @ObservedObject var musicManager = MusicManager.shared @State private var volumeSliderValue: Double = 0.5 @State private var dragging: Bool = false @State private var showVolumeSlider: Bool = false @State private var lastVolumeUpdateTime: Date = Date.distantPast private let volumeUpdateThrottle: TimeInterval = 0.1 var body: some View { HStack(spacing: 4) { Button(action: { if musicManager.volumeControlSupported { withAnimation(.easeInOut(duration: 0.12)) { showVolumeSlider.toggle() } } }) { Image(systemName: volumeIcon) .font(.system(size: 14, weight: .medium)) .foregroundColor(musicManager.volumeControlSupported ? .white : .gray) } .buttonStyle(PlainButtonStyle()) .disabled(!musicManager.volumeControlSupported) .frame(width: 24) if showVolumeSlider && musicManager.volumeControlSupported { CustomSlider( value: $volumeSliderValue, range: 0.0...1.0, color: .white, dragging: $dragging, lastDragged: .constant(Date.distantPast), onValueChange: { newValue in MusicManager.shared.setVolume(to: newValue) }, onDragChange: { newValue in let now = Date() if now.timeIntervalSince(lastVolumeUpdateTime) > volumeUpdateThrottle { MusicManager.shared.setVolume(to: newValue) lastVolumeUpdateTime = now } } ) .frame(width: 48, height: 8) .transition(.scale.combined(with: .opacity)) } } .clipped() .onReceive(musicManager.$volume) { volume in if !dragging { volumeSliderValue = volume } } .onReceive(musicManager.$volumeControlSupported) { supported in if !supported { withAnimation(.easeInOut(duration: 0.2)) { showVolumeSlider = false } } } .onChange(of: showVolumeSlider) { _, isShowing in if isShowing { // Sync volume from app when slider appears Task { await MusicManager.shared.syncVolumeFromActiveApp() } } } .onDisappear { // volumeUpdateTask?.cancel() // No longer needed } } private var volumeIcon: String { if !musicManager.volumeControlSupported { return "speaker.slash" } else if volumeSliderValue == 0 { return "speaker.slash.fill" } else if volumeSliderValue < 0.33 { return "speaker.1.fill" } else if volumeSliderValue < 0.66 { return "speaker.2.fill" } else { return "speaker.3.fill" } } } // MARK: - Main View struct NotchHomeView: View { @EnvironmentObject var vm: BoringViewModel @ObservedObject var webcamManager = WebcamManager.shared @ObservedObject var batteryModel = BatteryStatusViewModel.shared @ObservedObject var coordinator = BoringViewCoordinator.shared let albumArtNamespace: Namespace.ID var body: some View { Group { if !coordinator.firstLaunch { mainContent } } // simplified: use a straightforward opacity transition .transition(.opacity) } private var shouldShowCamera: Bool { Defaults[.showMirror] && webcamManager.cameraAvailable && vm.isCameraExpanded } private var mainContent: some View { HStack(alignment: .top, spacing: (shouldShowCamera && Defaults[.showCalendar]) ? 10 : 15) { MusicPlayerView(albumArtNamespace: albumArtNamespace) if Defaults[.showCalendar] { CalendarView() .frame(width: shouldShowCamera ? 170 : 215) .onHover { isHovering in vm.isHoveringCalendar = isHovering } .environmentObject(vm) .transition(.opacity) } if shouldShowCamera { CameraPreviewView(webcamManager: webcamManager) .scaledToFit() .opacity(vm.notchState == .closed ? 0 : 1) .blur(radius: vm.notchState == .closed ? 20 : 0) .animation(.interactiveSpring(response: 0.32, dampingFraction: 0.76, blendDuration: 0), value: shouldShowCamera) } } .transition(.asymmetric(insertion: .opacity.combined(with: .move(edge: .top)), removal: .opacity)) .blur(radius: vm.notchState == .closed ? 30 : 0) } } struct MusicSliderView: View { @Binding var sliderValue: Double @Binding var duration: Double @Binding var lastDragged: Date var color: NSColor @Binding var dragging: Bool let currentDate: Date let timestampDate: Date let elapsedTime: Double let playbackRate: Double let isPlaying: Bool var onValueChange: (Double) -> Void var body: some View { VStack { CustomSlider( value: $sliderValue, range: 0...duration, color: Defaults[.sliderColor] == SliderColorEnum.albumArt ? Color(nsColor: color).ensureMinimumBrightness(factor: 0.8) : Defaults[.sliderColor] == SliderColorEnum.accent ? .effectiveAccent : .white, dragging: $dragging, lastDragged: $lastDragged, onValueChange: onValueChange ) .frame(height: 10, alignment: .center) HStack { Text(timeString(from: sliderValue)) Spacer() Text(timeString(from: duration)) } .fontWeight(.medium) .foregroundColor( Defaults[.playerColorTinting] ? Color(nsColor: color).ensureMinimumBrightness(factor: 0.6) : .gray ) .font(.caption) } .onChange(of: currentDate) { guard !dragging, timestampDate.timeIntervalSince(lastDragged) > -1 else { return } sliderValue = MusicManager.shared.estimatedPlaybackPosition(at: currentDate) } } func timeString(from seconds: Double) -> String { let totalMinutes = Int(seconds) / 60 let remainingSeconds = Int(seconds) % 60 let hours = totalMinutes / 60 let minutes = totalMinutes % 60 if hours > 0 { return String(format: "%d:%02d:%02d", hours, minutes, remainingSeconds) } else { return String(format: "%d:%02d", minutes, remainingSeconds) } } } struct CustomSlider: View { @Binding var value: Double var range: ClosedRange var color: Color = .white @Binding var dragging: Bool @Binding var lastDragged: Date var onValueChange: ((Double) -> Void)? var onDragChange: ((Double) -> Void)? var body: some View { GeometryReader { geometry in let width = geometry.size.width let height = CGFloat(dragging ? 9 : 5) let rangeSpan = range.upperBound - range.lowerBound let progress = rangeSpan == .zero ? 0 : (value - range.lowerBound) / rangeSpan let filledTrackWidth = min(max(progress, 0), 1) * width ZStack(alignment: .leading) { Rectangle() .fill(.gray.opacity(0.3)) .frame(height: height) Rectangle() .fill(color) .frame(width: filledTrackWidth, height: height) } .cornerRadius(height / 2) .frame(height: 10) .contentShape(Rectangle()) .gesture( DragGesture(minimumDistance: 0) .onChanged { gesture in withAnimation { dragging = true } let newValue = range.lowerBound + Double(gesture.location.x / width) * rangeSpan value = min(max(newValue, range.lowerBound), range.upperBound) onDragChange?(value) } .onEnded { _ in onValueChange?(value) dragging = false lastDragged = Date() } ) .animation(.spring(response: 0.35, dampingFraction: 0.7), value: dragging) } } } ================================================ FILE: boringNotch/components/Notch/NotchShape.swift ================================================ // // NotchShape.swift // boringNotch // // Created by Kai Azim on 2023-08-24. // Original source: https://github.com/MrKai77/DynamicNotchKit // Modified by Alexander on 2025-05-18. import SwiftUI struct NotchShape: Shape { private var topCornerRadius: CGFloat private var bottomCornerRadius: CGFloat init( topCornerRadius: CGFloat? = nil, bottomCornerRadius: CGFloat? = nil ) { self.topCornerRadius = topCornerRadius ?? 6 self.bottomCornerRadius = bottomCornerRadius ?? 14 } var animatableData: AnimatablePair { get { .init( topCornerRadius, bottomCornerRadius ) } set { topCornerRadius = newValue.first bottomCornerRadius = newValue.second } } func path(in rect: CGRect) -> Path { var path = Path() path.move( to: CGPoint( x: rect.minX, y: rect.minY ) ) path.addQuadCurve( to: CGPoint( x: rect.minX + topCornerRadius, y: rect.minY + topCornerRadius ), control: CGPoint( x: rect.minX + topCornerRadius, y: rect.minY ) ) path.addLine( to: CGPoint( x: rect.minX + topCornerRadius, y: rect.maxY - bottomCornerRadius ) ) path.addQuadCurve( to: CGPoint( x: rect.minX + topCornerRadius + bottomCornerRadius, y: rect.maxY ), control: CGPoint( x: rect.minX + topCornerRadius, y: rect.maxY ) ) path.addLine( to: CGPoint( x: rect.maxX - topCornerRadius - bottomCornerRadius, y: rect.maxY ) ) path.addQuadCurve( to: CGPoint( x: rect.maxX - topCornerRadius, y: rect.maxY - bottomCornerRadius ), control: CGPoint( x: rect.maxX - topCornerRadius, y: rect.maxY ) ) path.addLine( to: CGPoint( x: rect.maxX - topCornerRadius, y: rect.minY + topCornerRadius ) ) path.addQuadCurve( to: CGPoint( x: rect.maxX, y: rect.minY ), control: CGPoint( x: rect.maxX - topCornerRadius, y: rect.minY ) ) path.addLine( to: CGPoint( x: rect.minX, y: rect.minY ) ) return path } } #Preview { NotchShape(topCornerRadius: 6, bottomCornerRadius: 14) .frame(width: 200, height: 32) .padding(10) } ================================================ FILE: boringNotch/components/Onboarding/MusicControllerSelectionView.swift ================================================ // // MusicControllerSelectionView.swift // boringNotch // // Created by Alexander on 2025-06-23. // import SwiftUI import Defaults struct MusicControllerSelectionView: View { let onContinue: () -> Void @Default(.mediaController) var mediaController private var availableMediaControllers: [MediaControllerType] { if MusicManager.shared.isNowPlayingDeprecated { return MediaControllerType.allCases.filter { $0 != .nowPlaying } } else { return MediaControllerType.allCases } } @State private var selectedMediaController: MediaControllerType = Defaults[.mediaController] var body: some View { VStack(spacing: 20) { Text("Choose a Music Source") .font(.title) .fontWeight(.bold) .padding(.top, 24) Text("Select the music source you want to use. You can change this later in the app settings.") .multilineTextAlignment(.center) .font(.body) .foregroundColor(.secondary) .padding(.horizontal) ScrollView { VStack(spacing: 12) { ForEach(availableMediaControllers) { controller in ControllerOptionView( controller: controller, isSelected: self.selectedMediaController == controller ) .onTapGesture { self.selectedMediaController = controller } } } .padding() } //Disable scroll if there are 4 or fewer to avoid unnecessary scroll behavior .scrollDisabled(availableMediaControllers.count <= 4) // Spacer() Button("Continue", action: { self.mediaController = self.selectedMediaController NotificationCenter.default.post( name: Notification.Name.mediaControllerChanged, object: nil ) onContinue() }) .buttonStyle(.borderedProminent) .controlSize(.large) .padding(.bottom, 24) } .frame(maxWidth: .infinity, maxHeight: .infinity) .background( VisualEffectView(material: .underWindowBackground, blendingMode: .behindWindow) .ignoresSafeArea() ) } } struct ControllerOptionView: View { let controller: MediaControllerType let isSelected: Bool var body: some View { HStack(spacing: 16) { Image(systemName: isSelected ? "checkmark.circle.fill" : "circle") .font(.title2) .foregroundColor(isSelected ? .effectiveAccent : .secondary.opacity(0.5)) .animation(.spring(response: 0.3, dampingFraction: 0.6), value: isSelected) VStack(alignment: .leading, spacing: 4) { Text(controller.rawValue) .font(.headline) .fontWeight(.semibold) Text(controller.description) .font(.subheadline) .foregroundColor(.secondary) if controller == .youtubeMusic, let url = URL(string: "https://github.com/pear-devs/pear-desktop") { Link("View on GitHub: pear-devs/pear-desktop", destination: url) .font(.subheadline) .padding(.top, 2) } } Spacer() } .padding() .background( RoundedRectangle(cornerRadius: 12, style: .continuous) .fill(isSelected ? Color.effectiveAccent.opacity(0.15) : Color.clear) ) .overlay( RoundedRectangle(cornerRadius: 12, style: .continuous) .stroke(isSelected ? Color.effectiveAccent : Color.secondary.opacity(0.3), lineWidth: 1.5) ) .contentShape(Rectangle()) } } extension MediaControllerType { var description: String { switch self { case .nowPlaying: return "Works with most media apps, including browsers, to detect what's playing. Note: This may be removed in a future macOS version." case .spotify: return "Connects directly to the Spotify app." case .appleMusic: return "Connects directly to the Apple Music app." case .youtubeMusic: return "Requires a third-party client with API plugin enabled." } } } #Preview { MusicControllerSelectionView(onContinue: {}) .frame(width: 400, height: 600) } ================================================ FILE: boringNotch/components/Onboarding/OnboardingFinishView.swift ================================================ // // OnboardingFinishView.swift // boringNotch // // Created by Alexander on 2025-06-23. // import SwiftUI struct OnboardingFinishView: View { let onFinish: () -> Void let onOpenSettings: () -> Void var body: some View { VStack(spacing: 20) { Spacer() Image(systemName: "sparkles") .font(.system(size: 60)) .foregroundColor(.effectiveAccent) .padding() Text("You're All Set!") .font(.largeTitle) .fontWeight(.bold) Text("You can now enjoy the app. If you want to tweak things further, you can always visit the settings.") .font(.body) .foregroundColor(.secondary) .multilineTextAlignment(.center) .padding(.horizontal, 40) Spacer() Spacer() VStack(spacing: 12) { Button(action: onOpenSettings) { Label("Customize in Settings", systemImage: "gear") .controlSize(.large) } .controlSize(.large) Button("Finish", action: onFinish) .buttonStyle(.borderedProminent) .controlSize(.large) .keyboardShortcut(.defaultAction) } .padding(24) } .frame(maxWidth: .infinity, maxHeight: .infinity) .background( VisualEffectView(material: .underWindowBackground, blendingMode: .behindWindow) .ignoresSafeArea() ) } } #Preview { OnboardingFinishView(onFinish: { }, onOpenSettings: { }) } ================================================ FILE: boringNotch/components/Onboarding/OnboardingView.swift ================================================ // // OnboardingView.swift // boringNotch // // Created by Alexander on 2025-06-23. // import SwiftUI import AVFoundation enum OnboardingStep { case welcome case cameraPermission case calendarPermission case remindersPermission case accessibilityPermission case musicPermission case finished } private let calendarService = CalendarService() struct OnboardingView: View { @State var step: OnboardingStep = .welcome let onFinish: () -> Void let onOpenSettings: () -> Void var body: some View { ZStack { switch step { case .welcome: WelcomeView { withAnimation(.easeInOut(duration: 0.6)) { step = .cameraPermission } } .transition(.opacity) case .cameraPermission: PermissionRequestView( icon: Image(systemName: "camera.fill"), title: "Enable Camera Access", description: "Boring Notch includes a mirror feature that lets you quickly check your appearance using your camera, right from the notch. Camera access is required only to show this live preview. You can turn the mirror feature on or off at any time in the app.", privacyNote: "Your camera is never used without your consent, and nothing is recorded or stored.", onAllow: { Task { await requestCameraPermission() withAnimation(.easeInOut(duration: 0.6)) { step = .calendarPermission } } }, onSkip: { withAnimation(.easeInOut(duration: 0.6)) { step = .calendarPermission } } ) .transition(.opacity) case .calendarPermission: PermissionRequestView( icon: Image(systemName: "calendar"), title: "Enable Calendar Access", description: "Boring Notch can show all your upcoming events in one place. Access to your calendar is needed to display your schedule.", privacyNote: "Your calendar data is only used to show your events and is never shared.", onAllow: { Task { await requestCalendarPermission() withAnimation(.easeInOut(duration: 0.6)) { step = .remindersPermission } } }, onSkip: { withAnimation(.easeInOut(duration: 0.6)) { step = .remindersPermission } } ) .transition(.opacity) case .remindersPermission: PermissionRequestView( icon: Image(systemName: "checklist"), title: "Enable Reminders Access", description: "Boring Notch can show your scheduled reminders alongside your calendar events. Access to Reminders is needed to display your reminders.", privacyNote: "Your reminders data is only used to show your reminders and is never shared.", onAllow: { Task { await requestRemindersPermission() withAnimation(.easeInOut(duration: 0.6)) { step = .accessibilityPermission } } }, onSkip: { withAnimation(.easeInOut(duration: 0.6)) { step = .accessibilityPermission } } ) .transition(.opacity) case .accessibilityPermission: PermissionRequestView( icon: Image(systemName: "hand.raised.fill"), title: "Enable Accessibility Access", description: "Accessibility access is required to replace system notifications with the Boring Notch HUD. This allows the app to intercept media and brightness events to display custom HUD overlays.", privacyNote: "Accessibility access is used only to improve media and brightness notifications. No data is collected or shared.", onAllow: { Task { await requestAccessibilityPermission() withAnimation(.easeInOut(duration: 0.6)) { step = .musicPermission } } }, onSkip: { withAnimation(.easeInOut(duration: 0.6)) { step = .musicPermission } } ) .transition(.opacity) case .musicPermission: MusicControllerSelectionView( onContinue: { withAnimation(.easeInOut(duration: 0.6)) { BoringViewCoordinator.shared.firstLaunch = false step = .finished } } ) .transition(.opacity) case .finished: OnboardingFinishView(onFinish: onFinish, onOpenSettings: onOpenSettings) } } .frame(width: 400, height: 600) } // MARK: - Permission Request Logic func requestCameraPermission() async { await AVCaptureDevice.requestAccess(for: .video) } func requestCalendarPermission() async { _ = try? await calendarService.requestAccess(to: .event) } func requestRemindersPermission() async { _ = try? await calendarService.requestAccess(to: .reminder) } func requestAccessibilityPermission() async { await XPCHelperClient.shared.ensureAccessibilityAuthorization(promptIfNeeded: true) } } ================================================ FILE: boringNotch/components/Onboarding/PermissionsRequestView.swift ================================================ // // PermissionsRequestView.swift // boringNotch // // Created by Alexander on 2025-06-23. // import SwiftUI struct PermissionRequestView: View { let icon: Image let title: String let description: String let privacyNote: String? let onAllow: () -> Void let onSkip: () -> Void var body: some View { VStack(spacing: 28) { icon .resizable() .scaledToFit() .frame(width: 70, height: 56) .foregroundColor(.effectiveAccent) .padding(.top, 32) Text(title) .font(.title) .fontWeight(.semibold) Text(description) .multilineTextAlignment(.center) .padding(.horizontal) if let privacyNote = privacyNote { HStack(spacing: 8) { Image(systemName: "lock.shield") .foregroundColor(.secondary) Text(privacyNote) .font(.subheadline) .foregroundColor(.secondary) .multilineTextAlignment(.leading) } .padding(.bottom, 8) .padding(.horizontal) } HStack { Button("Not Now") { onSkip() } .buttonStyle(.bordered) Button("Allow Access") { onAllow() } .buttonStyle(.borderedProminent) } .padding(.top, 10) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) .background( VisualEffectView(material: .underWindowBackground, blendingMode: .behindWindow) .ignoresSafeArea() ) } } ================================================ FILE: boringNotch/components/Onboarding/SparkleView.swift ================================================ // // SparkleView.swift // boringNotch // // Created by Richard Kunkli on 2024. 09. 26.. // import SwiftUI import AppKit class SparkleNSView: NSView { private var emitterLayer: CAEmitterLayer? override init(frame frameRect: NSRect) { super.init(frame: frameRect) self.wantsLayer = true setupEmitterLayer() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupEmitterLayer() { let emitterLayer = CAEmitterLayer() emitterLayer.emitterShape = .rectangle emitterLayer.emitterMode = .surface emitterLayer.renderMode = .oldestFirst let cell = CAEmitterCell() cell.contents = NSImage(named: "sparkle")?.cgImage(forProposedRect: nil, context: nil, hints: nil) cell.birthRate = 50 cell.lifetime = 5 cell.velocity = 10 cell.velocityRange = 5 cell.emissionRange = .pi * 2 cell.scale = 0.2 cell.scaleRange = 0.1 cell.alphaSpeed = -0.5 cell.yAcceleration = 10 // Add a slight downward motion emitterLayer.emitterCells = [cell] self.layer?.addSublayer(emitterLayer) self.emitterLayer = emitterLayer updateEmitterForCurrentBounds() } private func updateEmitterForCurrentBounds() { guard let emitterLayer = self.emitterLayer else { return } emitterLayer.frame = self.bounds emitterLayer.emitterSize = self.bounds.size emitterLayer.emitterPosition = CGPoint(x: bounds.width / 2, y: bounds.height / 2) // Adjust birth rate based on view size let area = bounds.width * bounds.height let baseBirthRate: Float = 50 let adjustedBirthRate = 20 // Assuming 200x200 as base size emitterLayer.emitterCells?.first?.birthRate = Float(adjustedBirthRate) } override func setFrameSize(_ newSize: NSSize) { super.setFrameSize(newSize) updateEmitterForCurrentBounds() } } struct SparkleView: NSViewRepresentable { func makeNSView(context: Context) -> SparkleNSView { return SparkleNSView() } func updateNSView(_ nsView: SparkleNSView, context: Context) {} } ================================================ FILE: boringNotch/components/Onboarding/WelcomeView.swift ================================================ // // WelcomeView.swift // boringNotch // // Created by Richard Kunkli on 2024. 09. 26.. // import SwiftUI import SwiftUIIntrospect struct WelcomeView: View { var onGetStarted: (() -> Void)? = nil var body: some View { ZStack(alignment: .top) { ZStack { Image("spotlight") .resizable() .aspectRatio(contentMode: .fit) .padding(.bottom) .blur(radius: 3) .offset(y: -5) .background(SparkleView().opacity(0.6)) VStack(spacing: 8) { Image("logo2") .resizable() .aspectRatio(contentMode: .fit) .frame(width: 100, height: 100) .padding(.bottom, 8) Text("Boring Notch") .font(.system(.largeTitle, design: .default)) .fontWeight(.semibold) Text("Welcome") .font(.title) .foregroundStyle(.secondary) .padding(.bottom, 30) if false { Text("PRO") .font(.system(size: 18, design: .rounded)) .fontWeight(.bold) .foregroundStyle(.white) .padding(.horizontal, 12) .padding(.vertical, 3) .background( Capsule() .fill(LinearGradient(colors: [.white.opacity(0.7), .white.opacity(0.3)], startPoint: .topLeading, endPoint: .bottomTrailing)) .strokeBorder(LinearGradient(stops: [.init(color: .white.opacity(0.7), location: 0.3), .init(color: .clear, location: 0.6)], startPoint: .topLeading, endPoint: .bottomTrailing)) .blendMode(.overlay) ) .padding(.bottom, 30) } Button { onGetStarted?() } label: { Text("Get started") .padding(.horizontal, 20) .padding(.vertical, 6) } .buttonStyle(BorderedProminentButtonStyle()) } .padding(.top) } Image("theboringteam") .resizable() .aspectRatio(contentMode: .fit) .frame(height: 22) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) .padding() .padding(.bottom, 36) .blendMode(.overlay) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .ignoresSafeArea() .background { VisualEffectView(material: .hudWindow, blendingMode: .behindWindow) .ignoresSafeArea() } } } #Preview { WelcomeView() } ================================================ FILE: boringNotch/components/ProgressIndicator.swift ================================================ // // ProgressIndicator.swift // boringNotch // // Created by Harsh Vardhan Goswami on 11/08/24. // import Foundation import SwiftUI struct CircularProgressView: View { let progress: Double let color: Color var body: some View { ZStack { Circle() .stroke( Color.white.opacity(0.2), lineWidth: 6 ) Circle() .trim(from: 0, to: progress) .stroke( color, // 1 style: StrokeStyle( lineWidth: 6, lineCap: .round ) ) .rotationEffect(.degrees(-90)) } } } enum ProgressIndicatorType { case circle case text } // based on type .circle or .text struct ProgressIndicator: View { var type: ProgressIndicatorType var progress: Double var color: Color var body: some View { switch type { case .circle: CircularProgressView(progress: progress, color: color).frame( width: 20, height: 20) case .text: Text("\(Int(progress * 100))%") } } } #Preview { ProgressIndicator(type: .circle, progress: 0.8, color: Color.blue).padding() .frame(width: 200, height: 200) } ================================================ FILE: boringNotch/components/Settings/EditPanelView.swift ================================================ // // EditPanelView.swift // boringNotch // // Created by Richard Kunkli on 12/08/2024. // import SwiftUI struct EditPanelView: View { @State var wallpaperPath: URL? var body: some View { VStack { HStack { Text("Edit layout") .font(.system(.largeTitle, design: .rounded)) .foregroundColor(.white.opacity(0.5)) Spacer() Button { exit(0) } label: { Label("Close", systemImage: "xmark") } .controlSize(.extraLarge) .buttonStyle(AccessoryBarButtonStyle()) } .padding() } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) } } #Preview { EditPanelView() } struct VisualEffectView: NSViewRepresentable { let material: NSVisualEffectView.Material let blendingMode: NSVisualEffectView.BlendingMode func makeNSView(context _: Context) -> NSVisualEffectView { let visualEffectView = NSVisualEffectView() visualEffectView.material = material visualEffectView.blendingMode = blendingMode visualEffectView.state = NSVisualEffectView.State.active visualEffectView.isEmphasized = true return visualEffectView } func updateNSView(_ visualEffectView: NSVisualEffectView, context _: Context) { visualEffectView.material = material visualEffectView.blendingMode = blendingMode } } ================================================ FILE: boringNotch/components/Settings/ListItemPopover.swift ================================================ // // ListItemPopover.swift // boringNotch // // Created by Richard Kunkli on 15/09/2024. // import SwiftUI struct ListItemPopover: View { let content: () -> Content @State private var isPresented: Bool = false var body: some View { Button { isPresented.toggle() } label: { Image(systemName: "info.circle") .foregroundStyle(.secondary) } .controlSize(.regular) .popover(isPresented: $isPresented, attachmentAnchor: .rect(.bounds), arrowEdge: .trailing, content: { content() .padding() }) } } ================================================ FILE: boringNotch/components/Settings/MusicSlotConfigurationView.swift ================================================ // // MusicSlotConfigurationView.swift // boringNotch // // Created by Alexander on 2025-11-17. // import Defaults import SwiftUI import UniformTypeIdentifiers struct MusicSlotConfigurationView: View { @Default(.musicControlSlots) private var musicControlSlots @ObservedObject private var musicManager = MusicManager.shared @State private var draggedSlot: MusicControlButton? private let fixedSlotCount: Int = 5 var body: some View { VStack(alignment: .leading, spacing: 12) { // Slot configuration (fixed 5) slotConfigurationSection // Reset button HStack { Spacer() Button("Reset to Defaults") { withAnimation { musicControlSlots = MusicControlButton.defaultLayout } } .buttonStyle(.borderless) } } .onAppear { ensureSlotCapacity(fixedSlotCount) } } private var previewSection: some View { HStack(alignment: .top, spacing: 12) { HStack(spacing: 6) { ForEach(0.. some View { let currentSlot = slotValue(at: index) return HStack(spacing: 12) { Text("\(index + 1)") .font(.system(size: 14, weight: .medium, design: .monospaced)) .foregroundStyle(.secondary) .frame(width: 20) Group { if currentSlot != .none { slotPreview(for: currentSlot) .frame(height: 32) .onDrag { DispatchQueue.main.async { draggedSlot = currentSlot } return NSItemProvider(object: NSString(string: "slot:\(index)")) } .onDrop(of: [UTType.plainText.identifier], isTargeted: nil) { providers in let handled = handleDrop(providers, toIndex: index) DispatchQueue.main.async { draggedSlot = nil } return handled } } else { // empty slot: allow drops but not dragging slotPreview(for: currentSlot) .frame(height: 32) .onDrop(of: [UTType.plainText.identifier], isTargeted: nil) { providers in let handled = handleDrop(providers, toIndex: index) DispatchQueue.main.async { draggedSlot = nil } return handled } } } Spacer() } .padding(8) .background(Color(NSColor.controlBackgroundColor).opacity(0.5)) .cornerRadius(6) } @ViewBuilder private func slotPreview(for slot: MusicControlButton) -> some View { ZStack { RoundedRectangle(cornerRadius: 8) .fill(Color(NSColor.controlBackgroundColor)) .frame(width: 44, height: 44) if slot != .none { Image(systemName: slot.iconName) .font(.system(size: slot.prefersLargeScale ? 18 : 15, weight: .medium)) .foregroundStyle(previewIconColor(for: slot)) .frame(width: 28, height: 28) } else { RoundedRectangle(cornerRadius: 6) .strokeBorder(style: StrokeStyle(lineWidth: 1, dash: [4, 4])) .foregroundStyle(Color.secondary.opacity(0.3)) .frame(width: 32, height: 32) } } .cornerRadius(8) .contentShape(RoundedRectangle(cornerRadius: 8)) } private func previewIconColor(for slot: MusicControlButton) -> Color { switch slot { case .shuffle: return musicManager.isShuffled ? .red : .primary case .repeatMode: return musicManager.repeatMode != .off ? .red : .primary case .favorite: return musicManager.isFavoriteTrack ? .red : .primary case .playPause: return .primary default: return .primary } } private func ensureSlotCapacity(_ target: Int) { guard target > musicControlSlots.count else { return } let missing = target - musicControlSlots.count musicControlSlots.append(contentsOf: Array(repeating: .none, count: missing)) } private func slotBinding(for index: Int) -> Binding { Binding( get: { slotValue(at: index) }, set: { newValue in updateSlot(newValue, at: index) } ) } private func slotValue(at index: Int) -> MusicControlButton { guard musicControlSlots.indices.contains(index) else { return .none } return musicControlSlots[index] } private func handleDrop(_ providers: [NSItemProvider], toIndex: Int) -> Bool { for provider in providers { if provider.canLoadObject(ofClass: NSString.self) { provider.loadObject(ofClass: NSString.self) { item, error in // item may be an NSString (which conforms to NSItemProviderReading) or other reading type if let nsstring = item as? NSString { let raw = nsstring as String DispatchQueue.main.async { processDropString(raw, toIndex: toIndex) } } else if let str = item as? String { DispatchQueue.main.async { processDropString(str, toIndex: toIndex) } } } return true } } return false } private func handleDropOnTrash(_ providers: [NSItemProvider]) -> Bool { for provider in providers { if provider.canLoadObject(ofClass: NSString.self) { provider.loadObject(ofClass: NSString.self) { item, error in if let nsstring = item as? NSString { let raw = nsstring as String DispatchQueue.main.async { if raw.hasPrefix("slot:") { // parse source slot index and clear it let from = Int(raw.replacingOccurrences(of: "slot:", with: "")) ?? -1 guard from >= 0 && from < fixedSlotCount else { return } var slots = musicControlSlots if from < slots.count { slots[from] = .none musicControlSlots = slots } } } } else if let str = item as? String { DispatchQueue.main.async { if str.hasPrefix("slot:") { let from = Int(str.replacingOccurrences(of: "slot:", with: "")) ?? -1 guard from >= 0 && from < fixedSlotCount else { return } var slots = musicControlSlots if from < slots.count { slots[from] = .none musicControlSlots = slots } } } } } return true } } return false } private func processDropString(_ raw: String, toIndex: Int) { if raw.hasPrefix("slot:") { let from = Int(raw.replacingOccurrences(of: "slot:", with: "")) ?? -1 guard from >= 0 && from < fixedSlotCount else { return } var slots = musicControlSlots if from < slots.count && toIndex < slots.count { slots.swapAt(from, toIndex) musicControlSlots = slots } } else if raw.hasPrefix("control:") { let val = raw.replacingOccurrences(of: "control:", with: "") if let control = MusicControlButton(rawValue: val) { // If this control already exists in another slot, clear that original slot var slots = musicControlSlots if let existing = slots.firstIndex(of: control), existing != toIndex { slots[existing] = .none musicControlSlots = slots } updateSlot(control, at: toIndex) } } } private func updateSlot(_ value: MusicControlButton, at index: Int) { var slots = musicControlSlots if index >= slots.count { slots.append(contentsOf: Array(repeating: .none, count: index - slots.count + 1)) } slots[index] = value musicControlSlots = slots } } ================================================ FILE: boringNotch/components/Settings/SettingsView.swift ================================================ // // SettingsView.swift // boringNotch // // Created by Richard Kunkli on 07/08/2024. // import AVFoundation import Defaults import EventKit import KeyboardShortcuts import LaunchAtLogin import Sparkle import SwiftUI import SwiftUIIntrospect struct SettingsView: View { @State private var selectedTab = "General" @State private var accentColorUpdateTrigger = UUID() let updaterController: SPUStandardUpdaterController? init(updaterController: SPUStandardUpdaterController? = nil) { self.updaterController = updaterController } var body: some View { NavigationSplitView { List(selection: $selectedTab) { NavigationLink(value: "General") { Label("General", systemImage: "gear") } NavigationLink(value: "Appearance") { Label("Appearance", systemImage: "eye") } NavigationLink(value: "Media") { Label("Media", systemImage: "play.laptopcomputer") } NavigationLink(value: "Calendar") { Label("Calendar", systemImage: "calendar") } NavigationLink(value: "HUD") { Label("HUDs", systemImage: "dial.medium.fill") } NavigationLink(value: "Battery") { Label("Battery", systemImage: "battery.100.bolt") } // NavigationLink(value: "Downloads") { // Label("Downloads", systemImage: "square.and.arrow.down") // } NavigationLink(value: "Shelf") { Label("Shelf", systemImage: "books.vertical") } NavigationLink(value: "Shortcuts") { Label("Shortcuts", systemImage: "keyboard") } // NavigationLink(value: "Extensions") { // Label("Extensions", systemImage: "puzzlepiece.extension") // } NavigationLink(value: "Advanced") { Label("Advanced", systemImage: "gearshape.2") } NavigationLink(value: "About") { Label("About", systemImage: "info.circle") } } .listStyle(SidebarListStyle()) .tint(.effectiveAccent) .toolbar(removing: .sidebarToggle) .navigationSplitViewColumnWidth(200) } detail: { Group { switch selectedTab { case "General": GeneralSettings() case "Appearance": Appearance() case "Media": Media() case "Calendar": CalendarSettings() case "HUD": HUD() case "Battery": Charge() case "Shelf": Shelf() case "Shortcuts": Shortcuts() case "Extensions": GeneralSettings() case "Advanced": Advanced() case "About": if let controller = updaterController { About(updaterController: controller) } else { // Fallback with a default controller About( updaterController: SPUStandardUpdaterController( startingUpdater: false, updaterDelegate: nil, userDriverDelegate: nil)) } default: GeneralSettings() } } .frame(maxWidth: .infinity, maxHeight: .infinity) } .navigationSplitViewStyle(.balanced) .toolbar(removing: .sidebarToggle) .toolbar { ToolbarItem(placement: .principal) { Text("") .frame(width: 0, height: 0) .accessibilityHidden(true) } } .formStyle(.grouped) .frame(width: 700) .background(Color(NSColor.windowBackgroundColor)) .tint(.effectiveAccent) .id(accentColorUpdateTrigger) .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("AccentColorChanged"))) { _ in accentColorUpdateTrigger = UUID() } } } struct GeneralSettings: View { @State private var screens: [(uuid: String, name: String)] = NSScreen.screens.compactMap { screen in guard let uuid = screen.displayUUID else { return nil } return (uuid, screen.localizedName) } @EnvironmentObject var vm: BoringViewModel @ObservedObject var coordinator = BoringViewCoordinator.shared @Default(.mirrorShape) var mirrorShape @Default(.showEmojis) var showEmojis @Default(.gestureSensitivity) var gestureSensitivity @Default(.minimumHoverDuration) var minimumHoverDuration @Default(.nonNotchHeight) var nonNotchHeight @Default(.nonNotchHeightMode) var nonNotchHeightMode @Default(.notchHeight) var notchHeight @Default(.notchHeightMode) var notchHeightMode @Default(.showOnAllDisplays) var showOnAllDisplays @Default(.automaticallySwitchDisplay) var automaticallySwitchDisplay @Default(.enableGestures) var enableGestures @Default(.openNotchOnHover) var openNotchOnHover var body: some View { Form { Section { Toggle(isOn: Binding( get: { Defaults[.menubarIcon] }, set: { Defaults[.menubarIcon] = $0 } )) { Text("Show menu bar icon") } .tint(.effectiveAccent) LaunchAtLogin.Toggle("Launch at login") Defaults.Toggle(key: .showOnAllDisplays) { Text("Show on all displays") } .onChange(of: showOnAllDisplays) { NotificationCenter.default.post( name: Notification.Name.showOnAllDisplaysChanged, object: nil) } Picker("Preferred display", selection: $coordinator.preferredScreenUUID) { ForEach(screens, id: \.uuid) { screen in Text(screen.name).tag(screen.uuid as String?) } } .onChange(of: NSScreen.screens) { screens = NSScreen.screens.compactMap { screen in guard let uuid = screen.displayUUID else { return nil } return (uuid, screen.localizedName) } } .disabled(showOnAllDisplays) Defaults.Toggle(key: .automaticallySwitchDisplay) { Text("Automatically switch displays") } .onChange(of: automaticallySwitchDisplay) { NotificationCenter.default.post( name: Notification.Name.automaticallySwitchDisplayChanged, object: nil) } .disabled(showOnAllDisplays) } header: { Text("System features") } Section { Picker( selection: $notchHeightMode, label: Text("Notch height on notch displays") ) { Text("Match real notch height") .tag(WindowHeightMode.matchRealNotchSize) Text("Match menu bar height") .tag(WindowHeightMode.matchMenuBar) Text("Custom height") .tag(WindowHeightMode.custom) } .onChange(of: notchHeightMode) { switch notchHeightMode { case .matchRealNotchSize: notchHeight = 38 case .matchMenuBar: notchHeight = 44 case .custom: notchHeight = 38 } NotificationCenter.default.post( name: Notification.Name.notchHeightChanged, object: nil) } if notchHeightMode == .custom { Slider(value: $notchHeight, in: 15...45, step: 1) { Text("Custom notch size - \(notchHeight, specifier: "%.0f")") } .onChange(of: notchHeight) { NotificationCenter.default.post( name: Notification.Name.notchHeightChanged, object: nil) } } Picker("Notch height on non-notch displays", selection: $nonNotchHeightMode) { Text("Match menubar height") .tag(WindowHeightMode.matchMenuBar) Text("Match real notch height") .tag(WindowHeightMode.matchRealNotchSize) Text("Custom height") .tag(WindowHeightMode.custom) } .onChange(of: nonNotchHeightMode) { switch nonNotchHeightMode { case .matchMenuBar: nonNotchHeight = 24 case .matchRealNotchSize: nonNotchHeight = 32 case .custom: nonNotchHeight = 32 } NotificationCenter.default.post( name: Notification.Name.notchHeightChanged, object: nil) } if nonNotchHeightMode == .custom { Slider(value: $nonNotchHeight, in: 0...40, step: 1) { Text("Custom notch size - \(nonNotchHeight, specifier: "%.0f")") } .onChange(of: nonNotchHeight) { NotificationCenter.default.post( name: Notification.Name.notchHeightChanged, object: nil) } } } header: { Text("Notch sizing") } NotchBehaviour() gestureControls() } .toolbar { Button("Quit app") { NSApp.terminate(self) } .controlSize(.extraLarge) } .accentColor(.effectiveAccent) .navigationTitle("General") .onChange(of: openNotchOnHover) { if !openNotchOnHover { enableGestures = true } } } @ViewBuilder func gestureControls() -> some View { Section { Defaults.Toggle(key: .enableGestures) { Text("Enable gestures") } .disabled(!openNotchOnHover) if enableGestures { Toggle("Change media with horizontal gestures", isOn: .constant(false)) .disabled(true) Defaults.Toggle(key: .closeGestureEnabled) { Text("Close gesture") } Slider(value: $gestureSensitivity, in: 100...300, step: 100) { HStack { Text("Gesture sensitivity") Spacer() Text( Defaults[.gestureSensitivity] == 100 ? "High" : Defaults[.gestureSensitivity] == 200 ? "Medium" : "Low" ) .foregroundStyle(.secondary) } } } } header: { HStack { Text("Gesture control") customBadge(text: "Beta") } } footer: { Text( "Two-finger swipe up on notch to close, two-finger swipe down on notch to open when **Open notch on hover** option is disabled" ) .multilineTextAlignment(.trailing) .foregroundStyle(.secondary) .font(.caption) } } @ViewBuilder func NotchBehaviour() -> some View { Section { Defaults.Toggle(key: .openNotchOnHover) { Text("Open notch on hover") } Defaults.Toggle(key: .enableHaptics) { Text("Enable haptic feedback") } Toggle("Remember last tab", isOn: $coordinator.openLastTabByDefault) if openNotchOnHover { Slider(value: $minimumHoverDuration, in: 0...1, step: 0.1) { HStack { Text("Hover delay") Spacer() Text("\(minimumHoverDuration, specifier: "%.1f")s") .foregroundStyle(.secondary) } } .onChange(of: minimumHoverDuration) { NotificationCenter.default.post( name: Notification.Name.notchHeightChanged, object: nil) } } } header: { Text("Notch behavior") } } } struct Charge: View { var body: some View { Form { Section { Defaults.Toggle(key: .showBatteryIndicator) { Text("Show battery indicator") } Defaults.Toggle(key: .showPowerStatusNotifications) { Text("Show power status notifications") } } header: { Text("General") } Section { Defaults.Toggle(key: .showBatteryPercentage) { Text("Show battery percentage") } Defaults.Toggle(key: .showPowerStatusIcons) { Text("Show power status icons") } } header: { Text("Battery Information") } } .onAppear { Task { @MainActor in await XPCHelperClient.shared.isAccessibilityAuthorized() } } .accentColor(.effectiveAccent) .navigationTitle("Battery") } } //struct Downloads: View { // @Default(.selectedDownloadIndicatorStyle) var selectedDownloadIndicatorStyle // @Default(.selectedDownloadIconStyle) var selectedDownloadIconStyle // var body: some View { // Form { // warningBadge("We don't support downloads yet", "It will be supported later on.") // Section { // Defaults.Toggle(key: .enableDownloadListener) { // Text("Show download progress") // } // .disabled(true) // Defaults.Toggle(key: .enableSafariDownloads) { // Text("Enable Safari Downloads") // } // .disabled(!Defaults[.enableDownloadListener]) // Picker("Download indicator style", selection: $selectedDownloadIndicatorStyle) { // Text("Progress bar") // .tag(DownloadIndicatorStyle.progress) // Text("Percentage") // .tag(DownloadIndicatorStyle.percentage) // } // Picker("Download icon style", selection: $selectedDownloadIconStyle) { // Text("Only app icon") // .tag(DownloadIconStyle.onlyAppIcon) // Text("Only download icon") // .tag(DownloadIconStyle.onlyIcon) // Text("Both") // .tag(DownloadIconStyle.iconAndAppIcon) // } // // } header: { // HStack { // Text("Download indicators") // comingSoonTag() // } // } // Section { // List { // ForEach([].indices, id: \.self) { index in // Text("\(index)") // } // } // .frame(minHeight: 96) // .overlay { // if true { // Text("No excluded apps") // .foregroundStyle(Color(.secondaryLabelColor)) // } // } // .actionBar(padding: 0) { // Group { // Button { // } label: { // Image(systemName: "plus") // .frame(width: 25, height: 16, alignment: .center) // .contentShape(Rectangle()) // .foregroundStyle(.secondary) // } // // Divider() // Button { // } label: { // Image(systemName: "minus") // .frame(width: 20, height: 16, alignment: .center) // .contentShape(Rectangle()) // .foregroundStyle(.secondary) // } // } // } // } header: { // HStack(spacing: 4) { // Text("Exclude apps") // comingSoonTag() // } // } // } // .navigationTitle("Downloads") // } //} struct HUD: View { @EnvironmentObject var vm: BoringViewModel @Default(.inlineHUD) var inlineHUD @Default(.enableGradient) var enableGradient @Default(.optionKeyAction) var optionKeyAction @Default(.hudReplacement) var hudReplacement @ObservedObject var coordinator = BoringViewCoordinator.shared @State private var accessibilityAuthorized = false var body: some View { Form { Section { HStack { VStack(alignment: .leading, spacing: 2) { Text("Replace system HUD") .font(.headline) Text("Replaces the standard macOS volume, display brightness, and keyboard brightness HUDs with a custom design.") .font(.subheadline) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) } Spacer(minLength: 40) Defaults.Toggle("", key: .hudReplacement) .labelsHidden() .toggleStyle(.switch) .controlSize(.large) .disabled(!accessibilityAuthorized) } if !accessibilityAuthorized { VStack(alignment: .leading, spacing: 8) { Text("Accessibility access is required to replace the system HUD.") .font(.subheadline) .foregroundStyle(.secondary) HStack(spacing: 12) { Button("Request Accessibility") { XPCHelperClient.shared.requestAccessibilityAuthorization() } .buttonStyle(.borderedProminent) } } .padding(.top, 6) } } Section { Picker("Option key behaviour", selection: $optionKeyAction) { ForEach(OptionKeyAction.allCases) { opt in Text(opt.rawValue).tag(opt) } } Picker("Progress bar style", selection: $enableGradient) { Text("Hierarchical") .tag(false) Text("Gradient") .tag(true) } Defaults.Toggle(key: .systemEventIndicatorShadow) { Text("Enable glowing effect") } Defaults.Toggle(key: .systemEventIndicatorUseAccent) { Text("Tint progress bar with accent color") } } header: { Text("General") } .disabled(!hudReplacement) Section { Defaults.Toggle(key: .showOpenNotchHUD) { Text("Show HUD in open notch") } Defaults.Toggle(key: .showOpenNotchHUDPercentage) { Text("Show percentage") } .disabled(!Defaults[.showOpenNotchHUD]) } header: { HStack { Text("Open Notch") customBadge(text: "Beta") } } .disabled(!hudReplacement) Section { Picker("HUD style", selection: $inlineHUD) { Text("Default") .tag(false) Text("Inline") .tag(true) } .onChange(of: Defaults[.inlineHUD]) { if Defaults[.inlineHUD] { withAnimation { Defaults[.systemEventIndicatorShadow] = false Defaults[.enableGradient] = false } } } Defaults.Toggle(key: .showClosedNotchHUDPercentage) { Text("Show percentage") } } header: { Text("Closed Notch") } .disabled(!Defaults[.hudReplacement]) } .accentColor(.effectiveAccent) .navigationTitle("HUDs") .task { accessibilityAuthorized = await XPCHelperClient.shared.isAccessibilityAuthorized() } .onAppear { XPCHelperClient.shared.startMonitoringAccessibilityAuthorization() } .onDisappear { XPCHelperClient.shared.stopMonitoringAccessibilityAuthorization() } .onReceive(NotificationCenter.default.publisher(for: .accessibilityAuthorizationChanged)) { notification in if let granted = notification.userInfo?["granted"] as? Bool { accessibilityAuthorized = granted } } } } struct Media: View { @Default(.waitInterval) var waitInterval @Default(.mediaController) var mediaController @ObservedObject var coordinator = BoringViewCoordinator.shared @Default(.hideNotchOption) var hideNotchOption @Default(.enableSneakPeek) private var enableSneakPeek @Default(.sneakPeekStyles) var sneakPeekStyles @Default(.enableLyrics) var enableLyrics var body: some View { Form { Section { Picker("Music Source", selection: $mediaController) { ForEach(availableMediaControllers) { controller in Text(controller.rawValue).tag(controller) } } .onChange(of: mediaController) { _, _ in NotificationCenter.default.post( name: Notification.Name.mediaControllerChanged, object: nil ) } } header: { Text("Media Source") } footer: { if MusicManager.shared.isNowPlayingDeprecated { HStack { Text("YouTube Music requires this third-party app to be installed: ") .foregroundStyle(.secondary) .font(.caption) Link( "https://github.com/pear-devs/pear-desktop", destination: URL(string: "https://github.com/pear-devs/pear-desktop")! ) .font(.caption) .foregroundColor(.blue) // Ensures it's visibly a link } } else { Text( "'Now Playing' was the only option on previous versions and works with all media apps." ) .foregroundStyle(.secondary) .font(.caption) } } Section { Toggle( "Show music live activity", isOn: $coordinator.musicLiveActivityEnabled.animation() ) Toggle("Show sneak peek on playback changes", isOn: $enableSneakPeek) Picker("Sneak Peek Style", selection: $sneakPeekStyles) { ForEach(SneakPeekStyle.allCases) { style in Text(style.rawValue).tag(style) } } HStack { Stepper(value: $waitInterval, in: 0...10, step: 1) { HStack { Text("Media inactivity timeout") Spacer() Text("\(Defaults[.waitInterval], specifier: "%.0f") seconds") .foregroundStyle(.secondary) } } } Picker( selection: $hideNotchOption, label: HStack { Text("Full screen behavior") customBadge(text: "Beta") } ) { Text("Hide for all apps").tag(HideNotchOption.always) Text("Hide for media app only").tag( HideNotchOption.nowPlayingOnly) Text("Never hide").tag(HideNotchOption.never) } } header: { Text("Media playback live activity") } Section { MusicSlotConfigurationView() Defaults.Toggle(key: .enableLyrics) { HStack { Text("Show lyrics below artist name") customBadge(text: "Beta") } } } header: { Text("Media controls") } footer: { Text("Customize which controls appear in the music player. Volume expands when active.") .font(.caption) .foregroundStyle(.secondary) } } .accentColor(.effectiveAccent) .navigationTitle("Media") } // Only show controller options that are available on this macOS version private var availableMediaControllers: [MediaControllerType] { if MusicManager.shared.isNowPlayingDeprecated { return MediaControllerType.allCases.filter { $0 != .nowPlaying } } else { return MediaControllerType.allCases } } } struct CalendarSettings: View { @ObservedObject private var calendarManager = CalendarManager.shared @Default(.showCalendar) var showCalendar: Bool @Default(.hideCompletedReminders) var hideCompletedReminders @Default(.hideAllDayEvents) var hideAllDayEvents @Default(.autoScrollToNextEvent) var autoScrollToNextEvent var body: some View { Form { Defaults.Toggle(key: .showCalendar) { Text("Show calendar") } Defaults.Toggle(key: .hideCompletedReminders) { Text("Hide completed reminders") } Defaults.Toggle(key: .hideAllDayEvents) { Text("Hide all-day events") } Defaults.Toggle(key: .autoScrollToNextEvent) { Text("Auto-scroll to next event") } Defaults.Toggle(key: .showFullEventTitles) { Text("Always show full event titles") } Section(header: Text("Calendars")) { if calendarManager.calendarAuthorizationStatus != .fullAccess { Text("Calendar access is denied. Please enable it in System Settings.") .foregroundColor(.red) .multilineTextAlignment(.center) .padding() Button("Open Calendar Settings") { if let settingsURL = URL( string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Calendars" ) { NSWorkspace.shared.open(settingsURL) } } } else { List { ForEach(calendarManager.eventCalendars, id: \.id) { calendar in Toggle( isOn: Binding( get: { calendarManager.getCalendarSelected(calendar) }, set: { isSelected in Task { await calendarManager.setCalendarSelected( calendar, isSelected: isSelected) } } ) ) { Text(calendar.title) } .accentColor(lighterColor(from: calendar.color)) .disabled(!showCalendar) } } } } Section(header: Text("Reminders")) { if calendarManager.reminderAuthorizationStatus != .fullAccess { Text("Reminder access is denied. Please enable it in System Settings.") .foregroundColor(.red) .multilineTextAlignment(.center) .padding() Button("Open Reminder Settings") { if let settingsURL = URL( string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Reminders" ) { NSWorkspace.shared.open(settingsURL) } } } else { List { ForEach(calendarManager.reminderLists, id: \.id) { calendar in Toggle( isOn: Binding( get: { calendarManager.getCalendarSelected(calendar) }, set: { isSelected in Task { await calendarManager.setCalendarSelected( calendar, isSelected: isSelected) } } ) ) { Text(calendar.title) } .accentColor(lighterColor(from: calendar.color)) .disabled(!showCalendar) } } } } } .accentColor(.effectiveAccent) .navigationTitle("Calendar") .onAppear { Task { await calendarManager.checkCalendarAuthorization() await calendarManager.checkReminderAuthorization() } } } } func lighterColor(from nsColor: NSColor, amount: CGFloat = 0.14) -> Color { let srgb = nsColor.usingColorSpace(.sRGB) ?? nsColor var (r, g, b, a): (CGFloat, CGFloat, CGFloat, CGFloat) = (0,0,0,0) srgb.getRed(&r, green: &g, blue: &b, alpha: &a) func lighten(_ c: CGFloat) -> CGFloat { let increased = c + (1.0 - c) * amount return min(max(increased, 0), 1) } let nr = lighten(r) let ng = lighten(g) let nb = lighten(b) return Color(red: Double(nr), green: Double(ng), blue: Double(nb), opacity: Double(a)) } struct About: View { @State private var showBuildNumber: Bool = false let updaterController: SPUStandardUpdaterController @Environment(\.openWindow) var openWindow var body: some View { VStack { Form { Section { HStack { Text("Release name") Spacer() Text(Defaults[.releaseName]) .foregroundStyle(.secondary) } HStack { Text("Version") Spacer() if showBuildNumber { Text("(\(Bundle.main.buildVersionNumber ?? ""))") .foregroundStyle(.secondary) } Text(Bundle.main.releaseVersionNumber ?? "unkown") .foregroundStyle(.secondary) } .onTapGesture { withAnimation { showBuildNumber.toggle() } } } header: { Text("Version info") } UpdaterSettingsView(updater: updaterController.updater) HStack(spacing: 30) { Spacer(minLength: 0) Button { if let url = URL(string: "https://github.com/TheBoredTeam/boring.notch") { NSWorkspace.shared.open(url) } } label: { VStack(spacing: 5) { Image("Github") .resizable() .aspectRatio(contentMode: .fit) .frame(width: 18) Text("GitHub") } .contentShape(Rectangle()) } Spacer(minLength: 0) } .buttonStyle(PlainButtonStyle()) } VStack(spacing: 0) { Divider() Text("Made with 🫶🏻 by not so boring not.people") .foregroundStyle(.secondary) .padding(.top, 5) .padding(.bottom, 7) .multilineTextAlignment(.center) .padding(.horizontal, 10) } .frame(maxWidth: .infinity, alignment: .center) } .toolbar { // Button("Welcome window") { // openWindow(id: "onboarding") // } // .controlSize(.extraLarge) CheckForUpdatesView(updater: updaterController.updater) } .navigationTitle("About") } } struct Shelf: View { @Default(.shelfTapToOpen) var shelfTapToOpen: Bool @Default(.quickShareProvider) var quickShareProvider @Default(.expandedDragDetection) var expandedDragDetection: Bool @StateObject private var quickShareService = QuickShareService.shared private var selectedProvider: QuickShareProvider? { quickShareService.availableProviders.first(where: { $0.id == quickShareProvider }) } init() { Task { await QuickShareService.shared.discoverAvailableProviders() } } var body: some View { Form { Section { Defaults.Toggle(key: .boringShelf) { Text("Enable shelf") } Defaults.Toggle(key: .openShelfByDefault) { Text("Open shelf by default if items are present") } Defaults.Toggle(key: .expandedDragDetection) { Text("Expanded drag detection area") } .onChange(of: expandedDragDetection) { NotificationCenter.default.post( name: Notification.Name.expandedDragDetectionChanged, object: nil ) } Defaults.Toggle(key: .copyOnDrag) { Text("Copy items on drag") } Defaults.Toggle(key: .autoRemoveShelfItems) { Text("Remove from shelf after dragging") } } header: { HStack { Text("General") } } Section { Picker("Quick Share Service", selection: $quickShareProvider) { ForEach(quickShareService.availableProviders, id: \.id) { provider in HStack { Group { if let imgData = provider.imageData, let nsImg = NSImage(data: imgData) { Image(nsImage: nsImg) .resizable() .aspectRatio(contentMode: .fit) } else { Image(systemName: "square.and.arrow.up") } } .frame(width: 16, height: 16) .foregroundColor(.accentColor) Text(provider.id) } .tag(provider.id) } } .pickerStyle(.menu) if let selectedProvider = selectedProvider { HStack { Group { if let imgData = selectedProvider.imageData, let nsImg = NSImage(data: imgData) { Image(nsImage: nsImg) .resizable() .aspectRatio(contentMode: .fit) } else { Image(systemName: "square.and.arrow.up") } } .frame(width: 16, height: 16) .foregroundColor(.accentColor) VStack(alignment: .leading, spacing: 2) { Text("Currently selected: \(selectedProvider.id)") .font(.caption) .foregroundColor(.secondary) Text("Files dropped on the shelf will be shared via this service") .font(.caption2) .foregroundColor(.secondary) } } .padding(.vertical, 4) } // Providers are always enabled; user can pick default service above. } header: { HStack { Text("Quick Share") } } footer: { Text("Choose which service to use when sharing files from the shelf. Click the shelf button to select files, or drag files onto it to share immediately.") .font(.caption) .foregroundColor(.secondary) } } .accentColor(.effectiveAccent) .navigationTitle("Shelf") } } //struct Extensions: View { // @State private var effectTrigger: Bool = false // var body: some View { // Form { // Section { // List { // ForEach(extensionManager.installedExtensions.indices, id: \.self) { index in // let item = extensionManager.installedExtensions[index] // HStack { // AppIcon(for: item.bundleIdentifier) // .resizable() // .frame(width: 24, height: 24) // Text(item.name) // ListItemPopover { // Text("Description") // } // Spacer(minLength: 0) // HStack(spacing: 6) { // Circle() // .frame(width: 6, height: 6) // .foregroundColor( // isExtensionRunning(item.bundleIdentifier) // ? .green : item.status == .disabled ? .gray : .red // ) // .conditionalModifier(isExtensionRunning(item.bundleIdentifier)) // { view in // view // .shadow(color: .green, radius: 3) // } // Text( // isExtensionRunning(item.bundleIdentifier) // ? "Running" // : item.status == .disabled ? "Disabled" : "Stopped" // ) // .contentTransition(.numericText()) // .foregroundStyle(.secondary) // .font(.footnote) // } // .frame(width: 60, alignment: .leading) // // Menu( // content: { // Button("Restart") { // let ws = NSWorkspace.shared // // if let ext = ws.runningApplications.first(where: { // $0.bundleIdentifier == item.bundleIdentifier // }) { // ext.terminate() // } // // if let appURL = ws.urlForApplication( // withBundleIdentifier: item.bundleIdentifier) // { // ws.openApplication( // at: appURL, configuration: .init(), // completionHandler: nil) // } // } // .keyboardShortcut("R", modifiers: .command) // Button("Disable") { // if let ext = NSWorkspace.shared.runningApplications.first( // where: { $0.bundleIdentifier == item.bundleIdentifier }) // { // ext.terminate() // } // extensionManager.installedExtensions[index].status = // .disabled // } // .keyboardShortcut("D", modifiers: .command) // Divider() // Button("Uninstall", role: .destructive) { // // // } // }, // label: { // Image(systemName: "ellipsis.circle") // .foregroundStyle(.secondary) // } // ) // .controlSize(.regular) // } // .buttonStyle(PlainButtonStyle()) // .padding(.vertical, 5) // } // } // .frame(minHeight: 120) // .actionBar { // Button { // } label: { // HStack(spacing: 3) { // Image(systemName: "plus") // Text("Add manually") // } // .foregroundStyle(.secondary) // } // .disabled(true) // Spacer() // Button { // withAnimation(.linear(duration: 1)) { // effectTrigger.toggle() // } completion: { // effectTrigger.toggle() // } // extensionManager.checkIfExtensionsAreInstalled() // } label: { // HStack(spacing: 3) { // Image(systemName: "arrow.triangle.2.circlepath") // .rotationEffect(effectTrigger ? .degrees(360) : .zero) // } // .foregroundStyle(.secondary) // } // } // .controlSize(.small) // .buttonStyle(PlainButtonStyle()) // .overlay { // if extensionManager.installedExtensions.isEmpty { // Text("No extension installed") // .foregroundStyle(Color(.secondaryLabelColor)) // .padding(.bottom, 22) // } // } // } header: { // HStack(spacing: 0) { // Text("Installed extensions") // if !extensionManager.installedExtensions.isEmpty { // Text(" – \(extensionManager.installedExtensions.count)") // .foregroundStyle(.secondary) // } // } // } // } // .accentColor(.effectiveAccent) // .navigationTitle("Extensions") // // TipsView() // // .padding(.horizontal, 19) // } //} struct Appearance: View { @ObservedObject var coordinator = BoringViewCoordinator.shared @Default(.mirrorShape) var mirrorShape @Default(.sliderColor) var sliderColor @Default(.useMusicVisualizer) var useMusicVisualizer @Default(.customVisualizers) var customVisualizers @Default(.selectedVisualizer) var selectedVisualizer let icons: [String] = ["logo2"] @State private var selectedIcon: String = "logo2" @State private var selectedListVisualizer: CustomVisualizer? = nil @State private var isPresented: Bool = false @State private var name: String = "" @State private var url: String = "" @State private var speed: CGFloat = 1.0 var body: some View { Form { Section { Toggle("Always show tabs", isOn: $coordinator.alwaysShowTabs) Defaults.Toggle(key: .settingsIconInNotch) { Text("Show settings icon in notch") } } header: { Text("General") } Section { Defaults.Toggle(key: .coloredSpectrogram) { Text("Colored spectrogram") } Defaults .Toggle("Player tinting", key: .playerColorTinting) Defaults.Toggle(key: .lightingEffect) { Text("Enable blur effect behind album art") } Picker("Slider color", selection: $sliderColor) { ForEach(SliderColorEnum.allCases, id: \.self) { option in Text(option.rawValue) } } } header: { Text("Media") } Section { Toggle( "Use music visualizer spectrogram", isOn: $useMusicVisualizer.animation() ) .disabled(true) if !useMusicVisualizer { if customVisualizers.count > 0 { Picker( "Selected animation", selection: $selectedVisualizer ) { ForEach( customVisualizers, id: \.self ) { visualizer in Text(visualizer.name) .tag(visualizer) } } } else { HStack { Text("Selected animation") Spacer() Text("No custom animation available") .foregroundStyle(.secondary) } } } } header: { HStack { Text("Custom music live activity animation") customBadge(text: "Coming soon") } } Section { List { ForEach(customVisualizers, id: \.self) { visualizer in HStack { LottieView( url: visualizer.url, speed: visualizer.speed, loopMode: .loop ) .frame(width: 30, height: 30, alignment: .center) Text(visualizer.name) Spacer(minLength: 0) if selectedVisualizer == visualizer { Text("selected") .font(.caption) .fontWeight(.medium) .foregroundStyle(.secondary) .padding(.trailing, 8) } } .buttonStyle(PlainButtonStyle()) .padding(.vertical, 2) .background( selectedListVisualizer != nil ? selectedListVisualizer == visualizer ? Color.effectiveAccent : Color.clear : Color.clear, in: RoundedRectangle(cornerRadius: 5) ) .contentShape(Rectangle()) .onTapGesture { if selectedListVisualizer == visualizer { selectedListVisualizer = nil return } selectedListVisualizer = visualizer } } } .safeAreaPadding( EdgeInsets(top: 5, leading: 0, bottom: 5, trailing: 0) ) .frame(minHeight: 120) .actionBar { HStack(spacing: 5) { Button { name = "" url = "" speed = 1.0 isPresented.toggle() } label: { Image(systemName: "plus") .foregroundStyle(.secondary) .contentShape(Rectangle()) } Divider() Button { if selectedListVisualizer != nil { let visualizer = selectedListVisualizer! selectedListVisualizer = nil customVisualizers.remove( at: customVisualizers.firstIndex(of: visualizer)!) if visualizer == selectedVisualizer && customVisualizers.count > 0 { selectedVisualizer = customVisualizers[0] } } } label: { Image(systemName: "minus") .foregroundStyle(.secondary) .contentShape(Rectangle()) } } } .controlSize(.small) .buttonStyle(PlainButtonStyle()) .overlay { if customVisualizers.isEmpty { Text("No custom visualizer") .foregroundStyle(Color(.secondaryLabelColor)) .padding(.bottom, 22) } } .sheet(isPresented: $isPresented) { VStack(alignment: .leading) { Text("Add new visualizer") .font(.largeTitle.bold()) .padding(.vertical) TextField("Name", text: $name) TextField("Lottie JSON URL", text: $url) HStack { Text("Speed") Spacer(minLength: 80) Text("\(speed, specifier: "%.1f")s") .multilineTextAlignment(.trailing) .foregroundStyle(.secondary) Slider(value: $speed, in: 0...2, step: 0.1) } .padding(.vertical) HStack { Button { isPresented.toggle() } label: { Text("Cancel") .frame(maxWidth: .infinity, alignment: .center) } Button { let visualizer: CustomVisualizer = .init( UUID: UUID(), name: name, url: URL(string: url)!, speed: speed ) if !customVisualizers.contains(visualizer) { customVisualizers.append(visualizer) } isPresented.toggle() } label: { Text("Add") .frame(maxWidth: .infinity, alignment: .center) } .buttonStyle(BorderedProminentButtonStyle()) } } .textFieldStyle(RoundedBorderTextFieldStyle()) .controlSize(.extraLarge) .padding() } } header: { HStack(spacing: 0) { Text("Custom vizualizers (Lottie)") if !Defaults[.customVisualizers].isEmpty { Text(" – \(Defaults[.customVisualizers].count)") .foregroundStyle(.secondary) } } } Section { Defaults.Toggle(key: .showMirror) { Text("Enable boring mirror") } .disabled(!checkVideoInput()) Picker("Mirror shape", selection: $mirrorShape) { Text("Circle") .tag(MirrorShapeEnum.circle) Text("Square") .tag(MirrorShapeEnum.rectangle) } Defaults.Toggle(key: .showNotHumanFace) { Text("Show cool face animation while inactive") } } header: { HStack { Text("Additional features") } } } .accentColor(.effectiveAccent) .navigationTitle("Appearance") } func checkVideoInput() -> Bool { if AVCaptureDevice.default(for: .video) != nil { return true } return false } } struct Advanced: View { @Default(.useCustomAccentColor) var useCustomAccentColor @Default(.customAccentColorData) var customAccentColorData @Default(.extendHoverArea) var extendHoverArea @Default(.showOnLockScreen) var showOnLockScreen @Default(.hideFromScreenRecording) var hideFromScreenRecording @State private var customAccentColor: Color = .accentColor @State private var selectedPresetColor: PresetAccentColor? = nil let icons: [String] = ["logo2"] @State private var selectedIcon: String = "logo2" // macOS accent colors enum PresetAccentColor: String, CaseIterable, Identifiable { case blue = "Blue" case purple = "Purple" case pink = "Pink" case red = "Red" case orange = "Orange" case yellow = "Yellow" case green = "Green" case graphite = "Graphite" var id: String { self.rawValue } var color: Color { switch self { case .blue: return Color(red: 0.0, green: 0.478, blue: 1.0) case .purple: return Color(red: 0.686, green: 0.322, blue: 0.871) case .pink: return Color(red: 1.0, green: 0.176, blue: 0.333) case .red: return Color(red: 1.0, green: 0.271, blue: 0.227) case .orange: return Color(red: 1.0, green: 0.584, blue: 0.0) case .yellow: return Color(red: 1.0, green: 0.8, blue: 0.0) case .green: return Color(red: 0.4, green: 0.824, blue: 0.176) case .graphite: return Color(red: 0.557, green: 0.557, blue: 0.576) } } } var body: some View { Form { Section { VStack(alignment: .leading, spacing: 16) { // Toggle between system and custom Picker("Accent color", selection: $useCustomAccentColor) { Text("System").tag(false) Text("Custom").tag(true) } .pickerStyle(.segmented) if !useCustomAccentColor { // System accent info VStack(alignment: .leading, spacing: 8) { HStack(spacing: 12) { AccentCircleButton( isSelected: true, color: .accentColor, isSystemDefault: true ) {} VStack(alignment: .leading, spacing: 2) { Text("Using System Accent") .font(.body) Text("Your macOS system accent color") .font(.caption) .foregroundStyle(.secondary) } Spacer() } } } else { // Custom color options VStack(alignment: .leading, spacing: 12) { Text("Color Presets") .font(.caption) .fontWeight(.semibold) .foregroundStyle(.secondary) HStack(spacing: 12) { ForEach(PresetAccentColor.allCases) { preset in AccentCircleButton( isSelected: selectedPresetColor == preset, color: preset.color, isMulticolor: false ) { selectedPresetColor = preset customAccentColor = preset.color saveCustomColor(preset.color) forceUiUpdate() } } Spacer() } Divider() .padding(.vertical, 4) // Custom color picker HStack(spacing: 12) { VStack(alignment: .leading, spacing: 2) { Text("Pick a Color") .font(.body) Text("Choose any color") .font(.caption) .foregroundStyle(.secondary) } Spacer() ColorPicker(selection: Binding( get: { customAccentColor }, set: { newColor in customAccentColor = newColor selectedPresetColor = nil saveCustomColor(newColor) forceUiUpdate() } ), supportsOpacity: false) { ZStack { Circle() .fill(customAccentColor) .frame(width: 32, height: 32) if selectedPresetColor == nil { Circle() .strokeBorder(.primary.opacity(0.3), lineWidth: 2) .frame(width: 32, height: 32) } } } .labelsHidden() } } } } .padding(.vertical, 4) } header: { Text("Accent color") } footer: { Text("Choose between your system accent color or customize it with your own selection.") .multilineTextAlignment(.trailing) .foregroundStyle(.secondary) .font(.caption) } .onAppear { initializeAccentColorState() } Section { Defaults.Toggle(key: .enableShadow) { Text("Enable window shadow") } Defaults.Toggle(key: .cornerRadiusScaling) { Text("Corner radius scaling") } } header: { Text("Window Appearance") } Section { HStack { ForEach(icons, id: \.self) { icon in Spacer() VStack { Image(icon) .resizable() .frame(width: 80, height: 80) .background( RoundedRectangle(cornerRadius: 20, style: .circular) .strokeBorder( icon == selectedIcon ? Color.effectiveAccent : .clear, lineWidth: 2.5 ) ) Text("Default") .fontWeight(.medium) .font(.caption) .foregroundStyle(icon == selectedIcon ? .white : .secondary) .padding(.horizontal, 10) .padding(.vertical, 3) .background( Capsule() .fill(icon == selectedIcon ? Color.effectiveAccent : .clear) ) } .onTapGesture { withAnimation { selectedIcon = icon } NSApp.applicationIconImage = NSImage(named: icon) } Spacer() } } .disabled(true) } header: { HStack { Text("App icon") customBadge(text: "Coming soon") } } Section { Defaults.Toggle(key: .extendHoverArea) { Text("Extend hover area") } Defaults.Toggle(key: .hideTitleBar) { Text("Hide title bar") } Defaults.Toggle(key: .showOnLockScreen) { Text("Show notch on lock screen") } Defaults.Toggle(key: .hideFromScreenRecording) { Text("Hide from screen recording") } } header: { Text("Window Behavior") } } .accentColor(.effectiveAccent) .navigationTitle("Advanced") .onAppear { loadCustomColor() } } private func forceUiUpdate() { // Force refresh the UI DispatchQueue.main.async { NotificationCenter.default.post(name: Notification.Name("AccentColorChanged"), object: nil) } } private func saveCustomColor(_ color: Color) { let nsColor = NSColor(color) if let colorData = try? NSKeyedArchiver.archivedData(withRootObject: nsColor, requiringSecureCoding: false) { Defaults[.customAccentColorData] = colorData forceUiUpdate() } } private func loadCustomColor() { if let colorData = Defaults[.customAccentColorData], let nsColor = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSColor.self, from: colorData) { customAccentColor = Color(nsColor: nsColor) // Check if loaded color matches a preset selectedPresetColor = nil for preset in PresetAccentColor.allCases { if colorsAreEqual(Color(nsColor: nsColor), preset.color) { selectedPresetColor = preset break } } } } private func colorsAreEqual(_ color1: Color, _ color2: Color) -> Bool { let nsColor1 = NSColor(color1).usingColorSpace(.sRGB) ?? NSColor(color1) let nsColor2 = NSColor(color2).usingColorSpace(.sRGB) ?? NSColor(color2) return abs(nsColor1.redComponent - nsColor2.redComponent) < 0.01 && abs(nsColor1.greenComponent - nsColor2.greenComponent) < 0.01 && abs(nsColor1.blueComponent - nsColor2.blueComponent) < 0.01 } private func initializeAccentColorState() { if !useCustomAccentColor { selectedPresetColor = nil // Multicolor is selected when useCustomAccentColor is false } else { loadCustomColor() } } } // MARK: - Accent Circle Button Component struct AccentCircleButton: View { let isSelected: Bool let color: Color var isSystemDefault: Bool = false var isMulticolor: Bool = false let action: () -> Void var body: some View { Button(action: action) { ZStack { // Color circle Circle() .fill(color) .frame(width: 32, height: 32) // Subtle border Circle() .strokeBorder(Color.primary.opacity(0.15), lineWidth: 1) .frame(width: 32, height: 32) // Apple-style highlight ring around the middle when selected if isSelected { Circle() .strokeBorder( Color.white.opacity(0.5), lineWidth: 2 ) .frame(width: 28, height: 28) } } } .buttonStyle(.plain) .help(isSystemDefault ? "Use your macOS system accent color" : "") } } struct Shortcuts: View { var body: some View { Form { Section { KeyboardShortcuts.Recorder("Toggle Sneak Peek:", name: .toggleSneakPeek) } header: { Text("Media") } footer: { Text( "Sneak Peek shows the media title and artist under the notch for a few seconds." ) .multilineTextAlignment(.trailing) .foregroundStyle(.secondary) .font(.caption) } Section { KeyboardShortcuts.Recorder("Toggle Notch Open:", name: .toggleNotchOpen) } } .accentColor(.effectiveAccent) .navigationTitle("Shortcuts") } } func proFeatureBadge() -> some View { Text("Upgrade to Pro") .foregroundStyle(Color(red: 0.545, green: 0.196, blue: 0.98)) .font(.footnote.bold()) .padding(.vertical, 3) .padding(.horizontal, 6) .background( RoundedRectangle(cornerRadius: 4).stroke( Color(red: 0.545, green: 0.196, blue: 0.98), lineWidth: 1)) } func comingSoonTag() -> some View { Text("Coming soon") .foregroundStyle(.secondary) .font(.footnote.bold()) .padding(.vertical, 3) .padding(.horizontal, 6) .background(Color(nsColor: .secondarySystemFill)) .clipShape(.capsule) } func customBadge(text: String) -> some View { Text(text) .foregroundStyle(.secondary) .font(.footnote.bold()) .padding(.vertical, 3) .padding(.horizontal, 6) .background(Color(nsColor: .secondarySystemFill)) .clipShape(.capsule) } func warningBadge(_ text: String, _ description: String) -> some View { Section { HStack(spacing: 12) { Image(systemName: "exclamationmark.triangle.fill") .font(.system(size: 22)) .foregroundStyle(.yellow) VStack(alignment: .leading) { Text(text) .font(.headline) Text(description) .foregroundStyle(.secondary) } Spacer() } } } #Preview { HUD() } ================================================ FILE: boringNotch/components/Settings/SettingsWindowController.swift ================================================ // // SettingsWindowController.swift // boringNotch // // Created by Alexander on 2025-06-14. // import AppKit import SwiftUI import Defaults import Sparkle class SettingsWindowController: NSWindowController { static let shared = SettingsWindowController() private var updaterController: SPUStandardUpdaterController? private init() { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 700, height: 600), styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], backing: .buffered, defer: false ) super.init(window: window) setupWindow() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func setUpdaterController(_ controller: SPUStandardUpdaterController) { self.updaterController = controller // Recreate the content view with the proper updater controller setupWindow() } private func setupWindow() { guard let window = window else { return } window.title = "Boring Notch Settings" window.titlebarAppearsTransparent = false window.titleVisibility = .visible window.toolbarStyle = .unified window.isMovableByWindowBackground = true // Make it behave like a regular app window with proper Spaces support window.collectionBehavior = [.managed, .participatesInCycle, .fullScreenAuxiliary] // Ensure proper window behavior window.hidesOnDeactivate = false window.isExcludedFromWindowsMenu = false // Configure window to be a standard document-style window window.isRestorable = true window.identifier = NSUserInterfaceItemIdentifier("BoringNotchSettingsWindow") // Create the SwiftUI content let settingsView = SettingsView(updaterController: updaterController) let hostingView = NSHostingView(rootView: settingsView) window.contentView = hostingView // Handle window closing window.delegate = self } func showWindow() { // Set app to regular mode first NSApp.setActivationPolicy(.regular) // If window is already visible, bring it to front properly if window?.isVisible == true { NSApp.activate(ignoringOtherApps: true) window?.orderFrontRegardless() window?.makeKeyAndOrderFront(nil) return } // Show the window with proper ordering window?.orderFrontRegardless() window?.makeKeyAndOrderFront(nil) window?.center() // Activate the app and ensure window gets focus NSApp.activate(ignoringOtherApps: true) // Force window to front after activation DispatchQueue.main.async { [weak self] in self?.window?.makeKeyAndOrderFront(nil) } } override func close() { super.close() relinquishFocus() } private func relinquishFocus() { window?.orderOut(nil) // Set app back to accessory mode immediately NSApp.setActivationPolicy(.accessory) } } extension SettingsWindowController: NSWindowDelegate { func windowWillClose(_ notification: Notification) { relinquishFocus() } func windowShouldClose(_ sender: NSWindow) -> Bool { return true } func windowDidBecomeKey(_ notification: Notification) { // Ensure app is in regular mode when window becomes key NSApp.setActivationPolicy(.regular) } func windowDidResignKey(_ notification: Notification) { } } ================================================ FILE: boringNotch/components/Settings/SoftwareUpdater.swift ================================================ // // SoftwareUpdater.swift // boringNotch // // Created by Richard Kunkli on 09/08/2024. // import SwiftUI import Sparkle final class CheckForUpdatesViewModel: ObservableObject { @Published var canCheckForUpdates = false init(updater: SPUUpdater) { updater.publisher(for: \.canCheckForUpdates) .assign(to: &$canCheckForUpdates) } } struct CheckForUpdatesView: View { @ObservedObject private var checkForUpdatesViewModel: CheckForUpdatesViewModel private let updater: SPUUpdater init(updater: SPUUpdater) { self.updater = updater // Create our view model for our CheckForUpdatesView self.checkForUpdatesViewModel = CheckForUpdatesViewModel(updater: updater) } var body: some View { Button("Check for Updates…", action: updater.checkForUpdates) .disabled(!checkForUpdatesViewModel.canCheckForUpdates) } } struct UpdaterSettingsView: View { private let updater: SPUUpdater @State private var automaticallyChecksForUpdates: Bool @State private var automaticallyDownloadsUpdates: Bool init(updater: SPUUpdater) { self.updater = updater self.automaticallyChecksForUpdates = updater.automaticallyChecksForUpdates self.automaticallyDownloadsUpdates = updater.automaticallyDownloadsUpdates } var body: some View { Section { Toggle("Automatically check for updates", isOn: $automaticallyChecksForUpdates) .onChange(of: automaticallyChecksForUpdates) { _, newValue in updater.automaticallyChecksForUpdates = newValue } Toggle("Automatically download updates", isOn: $automaticallyDownloadsUpdates) .disabled(!automaticallyChecksForUpdates) .onChange(of: automaticallyDownloadsUpdates) { _, newValue in updater.automaticallyDownloadsUpdates = newValue } } header: { HStack { Text("Software updates") } } } } ================================================ FILE: boringNotch/components/Shelf/Models/Bookmark.swift ================================================ // // Bookmark.swift // boringNotch // // Created by Alexander on 2025-10-08. // import Foundation import AppKit struct Bookmark: Sendable, Equatable, Codable { let data: Data init(data: Data) { self.data = data } init(url: URL) throws { guard url.isFileURL, FileManager.default.fileExists(atPath: url.path) else { throw NSError(domain: "Bookmark", code: 1, userInfo: [NSLocalizedDescriptionKey: "Not a valid file URL or file does not exist at \(url.path)"]) } do { let bookmark = try url.bookmarkData( options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil ) NSLog("✅ Successfully created bookmark for \(url.path)") self.data = bookmark } catch { NSLog("❌ Failed to create bookmark for \(url.path): \(error.localizedDescription)") throw error } } func resolve() -> (url: URL?, refreshedData: Data?) { guard !data.isEmpty else { return (nil, nil) } var isStale = false do { let url = try URL( resolvingBookmarkData: data, options: [.withSecurityScope], relativeTo: nil, bookmarkDataIsStale: &isStale ) if isStale, let newData = try? url.bookmarkData(options: [.withSecurityScope]) { NSLog("⚠️ Bookmark was stale for \(url.path), refreshed") return (url, newData) } return (url, nil) } catch { NSLog("❌ Failed to resolve bookmark: \(error.localizedDescription)") return (nil, nil) } } func resolveURL() -> URL? { return resolve().url } var refreshedData: Data? { return resolve().refreshedData } static func update(in items: inout [ShelfItem], for item: ShelfItem, newBookmark: Data) { guard let idx = items.firstIndex(where: { $0.id == item.id }) else { return } guard case .file = items[idx].kind else { return } items[idx].kind = ShelfItemKind.file(bookmark: newBookmark) } func validate() async -> Bool { let (url, _) = resolve() guard let url = url else { return false } return url.accessSecurityScopedResource { url in FileManager.default.fileExists(atPath: url.path) } } func withAccess(_ block: @Sendable (URL) async throws -> T) async rethrows -> T? { let url = resolveURL() guard let url = url else { return nil } return try await url.accessSecurityScopedResource { url in try await block(url) } } func withAccess(_ block: (URL) throws -> T) rethrows -> T? { let url = resolveURL() guard let url = url else { return nil } return try url.accessSecurityScopedResource { url in try block(url) } } } ================================================ FILE: boringNotch/components/Shelf/Models/ShelfItem.swift ================================================ // // ShelfItem.swift // boringNotch // // Created by Alexander on 2025-09-24. // import AppKit import Foundation enum ShelfItemKind: Codable, Equatable, Sendable { case file(bookmark: Data) case text(string: String) case link(url: URL) enum CodingKeys: String, CodingKey { case type, value } enum KindTag: String, Codable { case file, text, link } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let type = try container.decode(KindTag.self, forKey: .type) switch type { case .file: let data = try container.decode(Data.self, forKey: .value) self = .file(bookmark: data) case .text: self = .text(string: try container.decode(String.self, forKey: .value)) case .link: self = .link(url: try container.decode(URL.self, forKey: .value)) } } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) switch self { case .file(let bookmark): try container.encode(KindTag.file, forKey: .type) try container.encode(bookmark, forKey: .value) case .text(let string): try container.encode(KindTag.text, forKey: .type) try container.encode(string, forKey: .value) case .link(let url): try container.encode(KindTag.link, forKey: .type) try container.encode(url, forKey: .value) } } } @MainActor struct ShelfItem: Identifiable, Codable, Equatable, Sendable { let id: UUID var kind: ShelfItemKind var isTemporary: Bool init(id: UUID = UUID(), kind: ShelfItemKind, isTemporary: Bool = false) { self.id = id self.kind = kind self.isTemporary = isTemporary } var displayName: String { switch kind { case .file(let bookmarkData): let bookmark = Bookmark(data: bookmarkData) guard let resolvedURL = bookmark.resolveURL() else { return "" } // Check for stored data files (text blocks, weblocs, etc.) to provide friendly names if resolvedURL.pathExtension.lowercased() == "json" && resolvedURL.path.contains("TextBlocks") { do { let data = try Data(contentsOf: resolvedURL) let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 struct TextBlockData: Codable { let content: String let title: String? var displayTitle: String { if let title = title, !title.isEmpty { return title } let firstLine = content.components(separatedBy: .newlines).first ?? content if firstLine.count > 50 { return String(firstLine.prefix(47)) + "..." } return firstLine } } if let textData = try? decoder.decode(TextBlockData.self, from: data) { return textData.displayTitle } } catch { // Fall through to default naming } } else if resolvedURL.pathExtension.lowercased() == "webloc" && resolvedURL.path.contains("WebLocs") { do { let data = try Data(contentsOf: resolvedURL) if let plist = try PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any], let urlString = plist["URL"] as? String { let title = plist["Title"] as? String return title ?? urlString } } catch { // Fall through to default naming } } return (try? resolvedURL.resourceValues(forKeys: [.localizedNameKey]).localizedName) ?? resolvedURL.lastPathComponent case .text(let string): return string.trimmingCharacters(in: .whitespacesAndNewlines) case .link(let url): let s = url.absoluteString if s.hasPrefix("https://") { return String(s.dropFirst("https://".count)) } else if s.hasPrefix("http://") { return String(s.dropFirst("http://".count)) } else { return s } } } var fileURL: URL? { guard case .file = kind else { return nil } return ShelfStateViewModel.shared.resolveFileURL(for: self) } var URL: URL? { if case let .file(bookmark) = kind { return resolvedContext(for: bookmark)?.url } else if case let .link(url) = kind { return url } else { return nil } } var icon: NSImage { guard case .file = kind else { return Self.thumbnailSymbolImage(systemName: kind.iconSymbolName) ?? NSImage() } if let resolvedURL = ShelfStateViewModel.shared.resolveFileURL(for: self) { return NSWorkspace.shared.icon(forFile: resolvedURL.path) } return NSImage() } func cleanupStoredData() { guard case let .file(bookmark) = kind, let context = resolvedContext(for: bookmark) else { return } let url = context.url // Handle temporary files if isTemporary { TemporaryFileStorageService.shared.removeTemporaryFileIfNeeded(at: url) return } } } private extension ShelfItem { static func thumbnailSymbolImage( systemName: String, size: CGSize = CGSize(width: 64, height: 80), symbolPointSize: CGFloat = 38, backgroundColor: NSColor = NSColor.white, symbolColor: NSColor = NSColor.labelColor ) -> NSImage? { let image = NSImage(size: size) image.lockFocus() defer { image.unlockFocus() } let rect = CGRect(origin: .zero, size: size) let cornerRadius = min(size.width, size.height) * 0.06 let path = NSBezierPath(roundedRect: rect.insetBy(dx: 2, dy: 2), xRadius: cornerRadius, yRadius: cornerRadius) backgroundColor.setFill() path.fill() if let symbol = NSImage(systemSymbolName: systemName, accessibilityDescription: nil) { let symbolSize = CGSize(width: symbolPointSize, height: symbolPointSize) let symbolOrigin = CGPoint( x: (size.width - symbolSize.width) / 2, y: (size.height - symbolSize.height) / 2 ) let symbolRect = CGRect(origin: symbolOrigin, size: symbolSize) symbol.draw(in: symbolRect) } return image } } // MARK: - Identity key for deduplication extension ShelfItem { var identityKey: String { switch kind { case .file(let bookmark): if let url = resolvedContext(for: bookmark)?.url { return "file://" + url.standardizedFileURL.path } return "file://missing/" + bookmark.base64EncodedString() case .link(let u): return "link://" + u.absoluteString case .text(let s): return "text://" + s } } } // MARK: - Private helpers private extension ShelfItemKind { var iconSymbolName: String { switch self { case .file: return "questionmark.circle" case .text: return "text.justifyleft" case .link: return "link" } } } private extension ShelfItem { func resolvedContext(for bookmarkData: Data) -> (url: URL, bookmark: Data)? { let bookmark = Bookmark(data: bookmarkData) if let url = bookmark.resolveURL() { return (url, bookmark.refreshedData ?? bookmarkData) } return nil } } ================================================ FILE: boringNotch/components/Shelf/Services/ImageProcessingService.swift ================================================ // // ImageProcessingService.swift // boringNotch // // Created by Alexander on 2025-10-16. // import Foundation import AppKit import CoreImage import CoreGraphics import Vision import PDFKit import UniformTypeIdentifiers import ImageIO /// Options for image conversion struct ImageConversionOptions { enum ImageFormat { case png, jpeg, heic, tiff, bmp var utType: UTType { switch self { case .png: return .png case .jpeg: return .jpeg case .heic: return .heic case .tiff: return .tiff case .bmp: return .bmp } } var fileExtension: String { switch self { case .png: return "png" case .jpeg: return "jpg" case .heic: return "heic" case .tiff: return "tiff" case .bmp: return "bmp" } } } let format: ImageFormat let compressionQuality: Double // 0.0 to 1.0, only applies to JPEG/HEIC let maxDimension: CGFloat? // Max width or height, nil for no scaling let removeMetadata: Bool } /// Service for processing images (background removal, conversion, PDF creation) @MainActor final class ImageProcessingService { static let shared = ImageProcessingService() private init() {} private let ciContext = CIContext(options: nil) // MARK: - Remove Background /// Removes the background from an image using Vision framework func removeBackground(from url: URL) async throws -> URL? { guard let inputImage = NSImage(contentsOf: url) else { throw ImageProcessingError.invalidImage } guard let cgImage = inputImage.cgImage(forProposedRect: nil, context: nil, hints: nil) else { throw ImageProcessingError.invalidImage } let request = VNGenerateForegroundInstanceMaskRequest() let handler = VNImageRequestHandler(cgImage: cgImage) try handler.perform([request]) guard let result = request.results?.first else { throw ImageProcessingError.backgroundRemovalFailed } let mask = try result.generateScaledMaskForImage(forInstances: result.allInstances, from: handler) let output = try await applyMask(mask, to: cgImage) let processedImage = NSImage(cgImage: output, size: inputImage.size) // Create temporary file let originalName = url.deletingPathExtension().lastPathComponent let newName = "\(originalName)_no_bg.png" guard let pngData = processedImage.tiffRepresentation, let bitmap = NSBitmapImageRep(data: pngData), let finalData = bitmap.representation(using: .png, properties: [:]) else { throw ImageProcessingError.saveFailed } guard let tempURL = await TemporaryFileStorageService.shared.createTempFile( for: .data(finalData, suggestedName: newName) ) else { throw ImageProcessingError.saveFailed } return tempURL } private func applyMask(_ mask: CVPixelBuffer, to image: CGImage) async throws -> CGImage { let ciImage = CIImage(cgImage: image) let maskImage = CIImage(cvPixelBuffer: mask) let filter = CIFilter.blendWithMask() filter.inputImage = ciImage filter.maskImage = maskImage filter.backgroundImage = CIImage.empty() guard let output = filter.outputImage else { throw ImageProcessingError.backgroundRemovalFailed } let context = CIContext() guard let result = context.createCGImage(output, from: output.extent) else { throw ImageProcessingError.backgroundRemovalFailed } return result } // MARK: - Convert Image /// Converts an image with specified options func convertImage(from url: URL, options: ImageConversionOptions) async throws -> URL? { guard var inputImage = NSImage(contentsOf: url) else { throw ImageProcessingError.invalidImage } // Scale image if needed if let maxDim = options.maxDimension { inputImage = scaleImage(inputImage, maxDimension: maxDim) } // Get image data based on format let imageData: Data? if options.removeMetadata { // Create new image without metadata guard let cgImage = inputImage.cgImage(forProposedRect: nil, context: nil, hints: nil) else { throw ImageProcessingError.invalidImage } let newImage = NSImage(cgImage: cgImage, size: inputImage.size) imageData = try convertToFormat(newImage, format: options.format, quality: options.compressionQuality) } else { imageData = try convertToFormat(inputImage, format: options.format, quality: options.compressionQuality) } guard let data = imageData else { throw ImageProcessingError.conversionFailed } // Create temporary file let originalName = url.deletingPathExtension().lastPathComponent let newName = "\(originalName)_converted.\(options.format.fileExtension)" guard let tempURL = await TemporaryFileStorageService.shared.createTempFile( for: .data(data, suggestedName: newName) ) else { throw ImageProcessingError.saveFailed } return tempURL } private func convertToFormat(_ image: NSImage, format: ImageConversionOptions.ImageFormat, quality: Double) throws -> Data? { guard let tiffData = image.tiffRepresentation, let bitmap = NSBitmapImageRep(data: tiffData) else { return nil } switch format { case .png: return bitmap.representation(using: .png, properties: [:]) case .jpeg: let properties: [NSBitmapImageRep.PropertyKey: Any] = [ .compressionFactor: quality ] return bitmap.representation(using: .jpeg, properties: properties) case .tiff: let properties: [NSBitmapImageRep.PropertyKey: Any] = [ .compressionMethod: NSNumber(value: NSBitmapImageRep.TIFFCompression.lzw.rawValue) ] return bitmap.representation(using: .tiff, properties: properties) case .bmp: return bitmap.representation(using: .bmp, properties: [:]) case .heic: // HEIC requires using CIContext guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return nil } let ciImage = CIImage(cgImage: cgImage) let context = CIContext() let colorSpace = CGColorSpace(name: CGColorSpace.sRGB)! let options: [CIImageRepresentationOption: Any] = [ CIImageRepresentationOption(rawValue: kCGImageDestinationLossyCompressionQuality as String): quality ] return try? context.heifRepresentation(of: ciImage, format: .RGBA8, colorSpace: colorSpace, options: options) } } private func scaleImage(_ image: NSImage, maxDimension: CGFloat) -> NSImage { guard maxDimension > 0 else { return image } guard let srcCG = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return image } let srcMax = max(srcCG.width, srcCG.height) if CGFloat(srcMax) <= maxDimension { return image // no downscaling needed } let scale = maxDimension / CGFloat(srcMax) let ciImage = CIImage(cgImage: srcCG) let lanczos = CIFilter.lanczosScaleTransform() lanczos.inputImage = ciImage lanczos.scale = Float(scale) lanczos.aspectRatio = 1.0 guard let output = lanczos.outputImage else { return image } // Preserve the source color space for exact color matching let colorSpace = srcCG.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB)! let ciContext = CIContext(options: [.workingColorSpace: colorSpace]) // Render using the CIContext with matching color space guard let dstCG = ciContext.createCGImage(output, from: output.extent, format: .RGBA8, colorSpace: colorSpace) else { return image } return NSImage(cgImage: dstCG, size: NSSize(width: dstCG.width, height: dstCG.height)) } // MARK: - Create PDF /// Creates a PDF from multiple image URLs func createPDF(from imageURLs: [URL], outputName: String? = nil) async throws -> URL? { guard !imageURLs.isEmpty else { throw ImageProcessingError.noImagesProvided } let pdfDocument = PDFDocument() for (index, url) in imageURLs.enumerated() { guard let image = NSImage(contentsOf: url) else { continue } let pdfPage = PDFPage(image: image) if let page = pdfPage { pdfDocument.insert(page, at: index) } } guard pdfDocument.pageCount > 0 else { throw ImageProcessingError.pdfCreationFailed } // Create temporary file let name = outputName ?? "images_\(Date().timeIntervalSince1970).pdf" let pdfName = name.hasSuffix(".pdf") ? name : "\(name).pdf" guard let pdfData = pdfDocument.dataRepresentation() else { throw ImageProcessingError.pdfCreationFailed } guard let tempURL = await TemporaryFileStorageService.shared.createTempFile( for: .data(pdfData, suggestedName: pdfName) ) else { throw ImageProcessingError.saveFailed } return tempURL } // MARK: - Helper Methods /// Checks if a URL is an image file func isImageFile(_ url: URL) -> Bool { guard let contentType = try? url.resourceValues(forKeys: [.contentTypeKey]).contentType else { return false } return contentType.conforms(to: .image) } } // MARK: - Errors enum ImageProcessingError: LocalizedError { case invalidImage case backgroundRemovalFailed case conversionFailed case pdfCreationFailed case noImagesProvided case saveFailed var errorDescription: String? { switch self { case .invalidImage: return "The file is not a valid image" case .backgroundRemovalFailed: return "Failed to remove background from image" case .conversionFailed: return "Failed to convert image format" case .pdfCreationFailed: return "Failed to create PDF from images" case .noImagesProvided: return "No images were provided" case .saveFailed: return "Failed to save processed file" } } } ================================================ FILE: boringNotch/components/Shelf/Services/QuickLookService.swift ================================================ // // QuickLookService.swift // boringNotch // // Created by Alexander on 2025-10-07. // import Foundation import UniformTypeIdentifiers import SwiftUI import QuickLookUI import AppKit @MainActor final class QuickLookService: ObservableObject { @Published var urls: [URL] = [] @Published var selectedURL: URL? @Published var isQuickLookOpen: Bool = false private var previewPanel: QLPreviewPanel? private var dataSource: QuickLookDataSource? private var accessingURLs: [URL] = [] private var previewPanelObserver: Any? func show(urls: [URL], selectFirst: Bool = true, slideshow: Bool = false) { guard !urls.isEmpty else { return } stopAccessingCurrentURLs() accessingURLs = urls.filter { url in if url.isFileURL { return url.startAccessingSecurityScopedResource() } return true } self.urls = accessingURLs self.isQuickLookOpen = true if selectFirst { self.selectedURL = accessingURLs.first } // Observe the shared Quick Look preview panel closing so we can relinquish security scope let panel = QLPreviewPanel.shared() // Remove any existing observer for previous panel if let prev = previewPanel { NotificationCenter.default.removeObserver(self, name: NSWindow.willCloseNotification, object: prev) } previewPanel = panel NotificationCenter.default.addObserver(self, selector: #selector(previewPanelWillClose(_:)), name: NSWindow.willCloseNotification, object: panel) } func hide() { stopAccessingCurrentURLs() selectedURL = nil urls.removeAll() isQuickLookOpen = false if let panel = previewPanel, panel.isVisible { panel.orderOut(nil) } if let panel = previewPanel { NotificationCenter.default.removeObserver(self, name: NSWindow.willCloseNotification, object: panel) previewPanel = nil } } private func stopAccessingCurrentURLs() { NSLog("Stopping access to \(accessingURLs.count) URLs") for url in accessingURLs where url.isFileURL { url.stopAccessingSecurityScopedResource() } accessingURLs.removeAll() // If Quick Look panel was closed externally, also remove observer and clear reference if let panel = previewPanel { NotificationCenter.default.removeObserver(self, name: NSWindow.willCloseNotification, object: panel) previewPanel = nil } } func showQuickLook(urls: [URL]) { show(urls: urls, selectFirst: true, slideshow: false) } func updateSelection(urls: [URL]) { guard isQuickLookOpen else { return } show(urls: urls, selectFirst: true) } } extension QuickLookService { @objc private func previewPanelWillClose(_ notification: Notification) { guard let panel = notification.object as? QLPreviewPanel, panel === previewPanel else { return } // Ensure cleanup happens on main actor Task { @MainActor in stopAccessingCurrentURLs() selectedURL = nil urls.removeAll() isQuickLookOpen = false // Remove observer and clear reference NotificationCenter.default.removeObserver(self, name: NSWindow.willCloseNotification, object: panel) previewPanel = nil } } } struct QuickLookPresenter: ViewModifier { @ObservedObject var service: QuickLookService func body(content: Content) -> some View { content .quickLookPreview($service.selectedURL, in: service.urls) } } extension View { func quickLookPresenter(using service: QuickLookService) -> some View { self.modifier(QuickLookPresenter(service: service)) } } final class QuickLookDataSource: NSObject, QLPreviewPanelDataSource, QLPreviewPanelDelegate { private let urls: [URL] init(urls: [URL]) { self.urls = urls super.init() } nonisolated func numberOfPreviewItems(in panel: QLPreviewPanel!) -> Int { return urls.count } nonisolated func previewPanel(_ panel: QLPreviewPanel!, previewItemAt index: Int) -> QLPreviewItem! { guard index >= 0 && index < urls.count else { return nil } return urls[index] as QLPreviewItem } } ================================================ FILE: boringNotch/components/Shelf/Services/QuickShareService.swift ================================================ // // QuickShareService.swift // boringNotch // // Created by Alexander on 2025-09-24. // import AppKit import Foundation import UniformTypeIdentifiers /// Dynamic representation of a sharing provider discovered at runtime struct QuickShareProvider: Identifiable, Hashable, Sendable { var id: String var imageData: Data? var supportsRawText: Bool } class QuickShareService: ObservableObject { static let shared = QuickShareService() @Published var availableProviders: [QuickShareProvider] = [] @Published var isPickerOpen = false private var cachedServices: [String: NSSharingService] = [:] // Hold security-scoped URLs during sharing private var sharingAccessingURLs: [URL] = [] private var lifecycleDelegate: SharingLifecycleDelegate? init() { Task { await discoverAvailableProviders() } } // MARK: - Provider Discovery @MainActor func discoverAvailableProviders() async { let finder = ShareServiceFinder() // Use simple test items without creating actual temp files // This avoids issues with the Share Sheet retaining references to deleted files let testItems: [Any] = [ URL(string:"http://example.com") ?? URL(fileURLWithPath: "/"), "Test Text" as NSString ] let services = await finder.findApplicableServices(for: testItems) var providers: [QuickShareProvider] = [] for svc in services { let title = svc.title let imgData = svc.image.tiffRepresentation let supportsRawText = svc.canPerform(withItems: ["Test Text"]) let provider = QuickShareProvider(id: title, imageData: imgData, supportsRawText: supportsRawText) if !providers.contains(provider) { providers.append(provider) cachedServices[title] = svc } } if let idx = providers.firstIndex(where: { $0.id == "AirDrop" }) { let ad = providers.remove(at: idx) providers.insert(ad, at: 0) } if !providers.contains(where: { $0.id == "System Share Menu" }) { providers.append(QuickShareProvider(id: "System Share Menu", imageData: nil, supportsRawText: true)) } self.availableProviders = providers } // MARK: - File Picker @MainActor func showFilePicker(for provider: QuickShareProvider, from view: NSView?) async { guard !isPickerOpen else { print("⚠️ QuickShareService: File picker already open") return } isPickerOpen = true SharingStateManager.shared.beginInteraction() let panel = NSOpenPanel() panel.allowsMultipleSelection = true panel.canChooseDirectories = true panel.canChooseFiles = true panel.title = "Select Files for \(provider.id)" panel.message = "Choose files to share via \(provider.id)" let completion: (NSApplication.ModalResponse) -> Void = { [weak self] response in defer { self?.isPickerOpen = false SharingStateManager.shared.endInteraction() } if response == .OK && !panel.urls.isEmpty { Task { await self?.shareFilesOrText(panel.urls, using: provider, from: view) } } } let response = panel.runModal() completion(response) } // MARK: - Sharing @MainActor func shareFilesOrText(_ items: [Any], using provider: QuickShareProvider, from view: NSView?) async { let fileURLs = items.compactMap { $0 as? URL }.filter { $0.isFileURL } // Stop any previous sharing access stopSharingAccessingURLs() // Start security-scoped access for all file URLs sharingAccessingURLs = fileURLs.filter { $0.startAccessingSecurityScopedResource() } // Setup lifecycle delegate to keep notch open during picker/service let delegate = SharingStateManager.shared.makeDelegate { [weak self] in self?.lifecycleDelegate = nil self?.stopSharingAccessingURLs() } lifecycleDelegate = delegate if let svc = cachedServices[provider.id], svc.canPerform(withItems: items) { // For direct service path, explicitly mark service interaction start delegate.markServiceBegan() svc.delegate = delegate svc.perform(withItems: items) } else { let picker = NSSharingServicePicker(items: items) picker.delegate = delegate delegate.markPickerBegan() if let view { picker.show(relativeTo: .zero, of: view, preferredEdge: .minY) } } } private func stopSharingAccessingURLs() { NSLog("Stopping sharing access to URLs") for url in sharingAccessingURLs { url.stopAccessingSecurityScopedResource() } sharingAccessingURLs.removeAll() } // MARK: - SharingServiceDelegate private class SharingServiceDelegate: NSObject {} func shareDroppedFiles(_ providers: [NSItemProvider], using shareProvider: QuickShareProvider, from view: NSView?) async { var itemsToShare: [Any] = [] var foundText: String? for provider in providers { if let webURL = await provider.extractURL() { itemsToShare.append(webURL) } else if foundText == nil, let text = await provider.extractText() { foundText = text } else if let itemFileURL = await provider.extractItem() { let resolvedURL = await resolveShelfItemBookmark(for: itemFileURL) ?? itemFileURL itemsToShare.append(resolvedURL) } } // If text was found, prioritize sharing it. if let text = foundText { if shareProvider.supportsRawText { await shareFilesOrText([text], using: shareProvider, from: view) } else { if let tempTextURL = await TemporaryFileStorageService.shared.createTempFile(for: .text(text)) { await shareFilesOrText([tempTextURL], using: shareProvider, from: view) TemporaryFileStorageService.shared.removeTemporaryFileIfNeeded(at: tempTextURL) } else { await shareFilesOrText([text], using: shareProvider, from: view) } } } else if !itemsToShare.isEmpty { await shareFilesOrText(itemsToShare, using: shareProvider, from: view) } } private func resolveShelfItemBookmark(for fileURL: URL) async -> URL? { let items = await ShelfStateViewModel.shared.items for itm in items { if let resolved = await ShelfStateViewModel.shared.resolveAndUpdateBookmark(for: itm) { if resolved.standardizedFileURL.path == fileURL.standardizedFileURL.path { return resolved } } } print("❌ Failed to resolve bookmark for shelf item") return nil } } // MARK: - App Storage Extension for Provider Selection extension QuickShareProvider { static var defaultProvider: QuickShareProvider { let svc = QuickShareService.shared if let airdrop = svc.availableProviders.first(where: { $0.id == "AirDrop" }) { return airdrop } return svc.availableProviders.first ?? QuickShareProvider(id: "System Share Menu", imageData: nil, supportsRawText: true) } } ================================================ FILE: boringNotch/components/Shelf/Services/ShareServiceFinder.swift ================================================ // // ShareServiceFinder.swift // boringNotch // // Created by Alexander on 2025-10-06. // import Cocoa class ShareServiceFinder: NSObject, NSSharingServicePickerDelegate { @MainActor private var onServicesCaptured: (([NSSharingService]) -> Void)? /// Returns share services asynchronously without blocking the UI @MainActor func findApplicableServices(for items: [Any], timeout: TimeInterval = 2.0) async -> [NSSharingService] { let dummyView = NSView(frame: .zero) let picker = NSSharingServicePicker(items: items) picker.delegate = self return await withCheckedContinuation { continuation in var didResume = false // Capture services callback Task { @MainActor in self.onServicesCaptured = { services in guard !didResume else { return } didResume = true continuation.resume(returning: services) } } picker.show(relativeTo: dummyView.bounds, of: dummyView, preferredEdge: .minY) // Timeout task Task { @MainActor in try? await Task.sleep(for: .seconds(timeout)) guard !didResume else { return } didResume = true print("Warning: timed out waiting for sharing services") continuation.resume(returning: []) } } } // MARK: NSSharingServicePickerDelegate func sharingServicePicker(_ picker: NSSharingServicePicker, sharingServicesForItems items: [Any], proposedSharingServices proposed: [NSSharingService]) -> [NSSharingService] { Task { @MainActor in self.onServicesCaptured?(proposed) } return proposed } } ================================================ FILE: boringNotch/components/Shelf/Services/ShelfActionService.swift ================================================ // // ShelfActionService.swift // boringNotch // // Created by Alexander on 2025-10-07. // import AppKit import Foundation /// A service providing common actions for `ShelfItem`s, such as opening, revealing, or copying paths. @MainActor enum ShelfActionService { static func open(_ item: ShelfItem) { switch item.kind { case .file(let bookmark): handleBookmarkedFile(bookmark) { url in NSWorkspace.shared.open(url) } case .link(let url): NSWorkspace.shared.open(url) case .text(let string): NSPasteboard.general.clearContents() NSPasteboard.general.setString(string, forType: .string) } } static func reveal(_ item: ShelfItem) { guard case .file(let bookmark) = item.kind else { return } handleBookmarkedFile(bookmark) { url in NSWorkspace.shared.activateFileViewerSelecting([url]) } } static func copyPath(_ item: ShelfItem) { guard case .file(let bookmark) = item.kind else { return } handleBookmarkedFile(bookmark) { url in NSPasteboard.general.clearContents() NSPasteboard.general.setString(url.path, forType: .string) } } static func remove(_ item: ShelfItem) { ShelfStateViewModel.shared.remove(item) } private static func handleBookmarkedFile(_ bookmarkData: Data, action: @escaping @Sendable (URL) -> Void) { Task { let bookmark = Bookmark(data: bookmarkData) if let url = bookmark.resolveURL() { url.accessSecurityScopedResource { accessibleURL in action(accessibleURL) } } } } } ================================================ FILE: boringNotch/components/Shelf/Services/ShelfDropService.swift ================================================ // // ShelfDropService.swift // boringNotch // // Created by Alexander on 2025-09-26. // import AppKit import Foundation import UniformTypeIdentifiers struct ShelfDropService { static func items(from providers: [NSItemProvider]) async -> [ShelfItem] { var results: [ShelfItem] = [] for provider in providers { if let item = await processProvider(provider) { results.append(item) } } return results } private static func processProvider(_ provider: NSItemProvider) async -> ShelfItem? { if let actualFileURL = await provider.extractFileURL() { if let bookmark = createBookmark(for: actualFileURL) { return await ShelfItem(kind: .file(bookmark: bookmark), isTemporary: false) } return nil } if let url = await provider.extractURL() { if url.isFileURL { if let bookmark = createBookmark(for: url) { return await ShelfItem(kind: .file(bookmark: bookmark), isTemporary: false) } } else { return await ShelfItem(kind: .link(url: url), isTemporary: false) } return nil } if let text = await provider.extractText() { return await ShelfItem(kind: .text(string: text), isTemporary: false) } if let data = await provider.loadData() { if let tempDataURL = await TemporaryFileStorageService.shared.createTempFile(for: .data(data, suggestedName: provider.suggestedName)), let bookmark = createBookmark(for: tempDataURL) { return await ShelfItem(kind: .file(bookmark: bookmark), isTemporary: true) } return nil } if let fileURL = await provider.extractItem() { if let bookmark = createBookmark(for: fileURL) { return await ShelfItem(kind: .file(bookmark: bookmark), isTemporary: false) } } return nil } private static func createBookmark(for url: URL) -> Data? { return (try? Bookmark(url: url))?.data } } ================================================ FILE: boringNotch/components/Shelf/Services/ShelfPersistenceService.swift ================================================ // // ShelfPersistenceService.swift // boringNotch // // Created by Alexander on 2025-09-24. // import Foundation // Access model types @_exported import struct Foundation.URL final class ShelfPersistenceService { static let shared = ShelfPersistenceService() private let fileURL: URL private let encoder = JSONEncoder() private let decoder = JSONDecoder() private init() { let fm = FileManager.default let support = try? fm.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true) let dir = (support ?? fm.temporaryDirectory).appendingPathComponent("boringNotch", isDirectory: true).appendingPathComponent("Shelf", isDirectory: true) try? fm.createDirectory(at: dir, withIntermediateDirectories: true) fileURL = dir.appendingPathComponent("items.json") encoder.outputFormatting = [.prettyPrinted] decoder.dateDecodingStrategy = .iso8601 encoder.dateEncodingStrategy = .iso8601 } func load() -> [ShelfItem] { guard let data = try? Data(contentsOf: fileURL) else { return [] } // Try to decode as array first (normal case) if let items = try? decoder.decode([ShelfItem].self, from: data) { return items } // If array decoding fails, try to decode individual items do { // Parse as JSON array to get individual item data guard let jsonArray = try JSONSerialization.jsonObject(with: data) as? [Any] else { print("⚠️ Shelf persistence file is not a valid JSON array") return [] } var validItems: [ShelfItem] = [] var failedCount = 0 for (index, jsonItem) in jsonArray.enumerated() { do { let itemData = try JSONSerialization.data(withJSONObject: jsonItem) let item = try decoder.decode(ShelfItem.self, from: itemData) validItems.append(item) } catch { failedCount += 1 print("⚠️ Failed to decode shelf item at index \(index): \(error.localizedDescription)") } } if failedCount > 0 { print("📦 Successfully loaded \(validItems.count) shelf items, discarded \(failedCount) corrupted items") } return validItems } catch { print("❌ Failed to parse shelf persistence file: \(error.localizedDescription)") return [] } } func save(_ items: [ShelfItem]) { do { let data = try encoder.encode(items) try data.write(to: fileURL, options: Data.WritingOptions.atomic) } catch { print("Failed to save shelf items: \(error.localizedDescription)") } } } ================================================ FILE: boringNotch/components/Shelf/Services/TemporaryFileStorageService.swift ================================================ // // TemporaryFileStorageService.swift // boringNotch // // Created by Alexander on 2025-09-24. // import Foundation import AppKit import UniformTypeIdentifiers enum TempFileType { case data(Data, suggestedName: String?) case text(String) case url(URL) } class TemporaryFileStorageService { static let shared = TemporaryFileStorageService() // MARK: - Public Interface /// Creates a temporary file and tracks it for manual cleanup func createTempFile(for type: TempFileType) async -> URL? { return await withCheckedContinuation { continuation in let result = createTempFile(for: type) continuation.resume(returning: result) } } func removeTemporaryFileIfNeeded(at url: URL) { let tempDirectory = URL(fileURLWithPath: NSTemporaryDirectory()) guard url.path.hasPrefix(tempDirectory.path) else { print("Attempted to remove temporary file outside temp directory: \(url.path)") return } let folderURL = url.deletingLastPathComponent() do { try FileManager.default.removeItem(at: url) print("Deleted file: \(url.path)") let contents = try FileManager.default.contentsOfDirectory(atPath: folderURL.path) if contents.isEmpty { try FileManager.default.removeItem(at: folderURL) print("Folder was empty, deleted folder: \(folderURL.path)") } else { print("Folder not deleted — it still contains \(contents.count) item(s).") } } catch { print("Error: \(error.localizedDescription)") } } // MARK: - Private Implementation private func createTempFile(for type: TempFileType) -> URL? { let tempDir = URL(fileURLWithPath: NSTemporaryDirectory()) let uuid = UUID().uuidString switch type { case .data(let data, let suggestedName): let filename = suggestedName ?? ".dat" let dirURL = tempDir.appendingPathComponent(uuid, isDirectory: true) let fileURL = dirURL.appendingPathComponent(filename) do { try FileManager.default.createDirectory(at: dirURL, withIntermediateDirectories: true) try data.write(to: fileURL) return fileURL } catch { print("Error: \(error)") return nil } case .text(let string): let filename = "\(uuid).txt" let dirURL = tempDir.appendingPathComponent(uuid, isDirectory: true) let fileURL = dirURL.appendingPathComponent(filename) guard let data = string.data(using: .utf8) else { print("❌ Failed to convert text to data") return nil } do { try FileManager.default.createDirectory(at: dirURL, withIntermediateDirectories: true) try data.write(to: fileURL) return fileURL } catch { print("Error: \(error)") return nil } case .url(let url): let filename = "\(url.host ?? uuid).webloc" let dirURL = tempDir.appendingPathComponent(uuid, isDirectory: true) let fileURL = dirURL.appendingPathComponent(filename) let weblocContent = createWeblocContent(for: url) guard let data = weblocContent.data(using: String.Encoding.utf8) else { print("❌ Failed to create webloc data") return nil } do { try FileManager.default.createDirectory(at: dirURL, withIntermediateDirectories: true) try data.write(to: fileURL) return fileURL } catch { print("Error: \(error)") return nil } } } private func createFile(at url: URL, data: Data) -> URL? { do { try data.write(to: url) return url } catch { print("❌ Failed to create temp file at \(url.path): \(error)") return nil } } func createZip(from urls: [URL], suggestedName: String? = nil) async -> URL? { let tempDir = URL(fileURLWithPath: NSTemporaryDirectory()) let uuid = UUID().uuidString let workingDir = tempDir.appendingPathComponent("zip_\(uuid)", isDirectory: true) do { try FileManager.default.createDirectory(at: workingDir, withIntermediateDirectories: true) } catch { print("❌ Failed to create zip working directory: \(error)") return nil } // Helper to run zip process func runZip(arguments: [String], currentDirectory: URL) -> Bool { let proc = Process() proc.executableURL = URL(fileURLWithPath: "/usr/bin/zip") proc.arguments = arguments proc.currentDirectoryURL = currentDirectory do { try proc.run() proc.waitUntilExit() return proc.terminationStatus == 0 } catch { print("❌ Failed to run zip: \(error)") return false } } // Single-item optimization: do not copy contents into the working dir. if urls.count == 1, let src = urls.first { let isDir = (try? src.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false let baseName = src.lastPathComponent let archiveName: String if isDir { // Folder: name as FolderName.zip and include the folder itself in the archive archiveName = "\(baseName).zip" let archiveURL = workingDir.appendingPathComponent(archiveName) // Run zip from the parent directory so the folder is stored as top-level entry let parent = src.deletingLastPathComponent() let args = ["-r", "-q", archiveURL.path, baseName] let ok = runZip(arguments: args, currentDirectory: parent) if ok { return archiveURL } else { return nil } } else { // File: include the file only (no parent folders). Name should include original extension. archiveName = "\(baseName).zip" let archiveURL = workingDir.appendingPathComponent(archiveName) let parent = src.deletingLastPathComponent() // -j to junk paths and store only the file let args = ["-j", "-q", archiveURL.path, baseName] let ok = runZip(arguments: args, currentDirectory: parent) if ok { return archiveURL } else { return nil } } } // Multi-item: copy items into working dir (so their relative structure is preserved), zip, then remove copies. for src in urls { let dest = workingDir.appendingPathComponent(src.lastPathComponent) do { if FileManager.default.fileExists(atPath: dest.path) { // Avoid collision by appending a suffix let unique = "\(UUID().uuidString)_\(src.lastPathComponent)" try FileManager.default.copyItem(at: src, to: workingDir.appendingPathComponent(unique)) } else { try FileManager.default.copyItem(at: src, to: dest) } } catch { print("⚠️ Failed to copy \(src.path) to working dir: \(error)") } } let archiveName = suggestedName ?? "Archive.zip" let archiveURL = workingDir.appendingPathComponent(archiveName) let args = ["-r", "-q", archiveURL.path, "."] let ok = runZip(arguments: args, currentDirectory: workingDir) if ok { // Remove the copied (uncompressed) items so the temp folder contains only the archive do { let contents = try FileManager.default.contentsOfDirectory(at: workingDir, includingPropertiesForKeys: nil) for file in contents { if file.standardizedFileURL != archiveURL.standardizedFileURL { try FileManager.default.removeItem(at: file) } } } catch { print("⚠️ Failed to cleanup working directory after zip: \(error)") } return archiveURL } else { return nil } } // MARK: - Content Creation Helpers private func createWeblocContent(for url: URL) -> String { return """ URL \(url.absoluteString) """ } } ================================================ FILE: boringNotch/components/Shelf/Services/ThumbnailService.swift ================================================ // // ThumbnailService.swift // boringNotch // // Created by Alexander on 2025-10-07. // import Foundation import AppKit import QuickLookThumbnailing import UniformTypeIdentifiers actor ThumbnailService { static let shared = ThumbnailService() private var cache: [String: NSImage] = [:] private var pendingRequests: [String: Task] = [:] private let thumbnailGenerator = QLThumbnailGenerator.shared private init() {} func thumbnail(for url: URL, size: CGSize) async -> NSImage? { let cacheKey = "\(url.path)_\(size.width)x\(size.height)" if let cached = cache[cacheKey] { return cached } if let pending = pendingRequests[cacheKey] { return await pending.value } let task = Task { let thumbnail = await generateQuickLookThumbnail(for: url, size: size) if let thumbnail = thumbnail { cache[cacheKey] = thumbnail } pendingRequests[cacheKey] = nil return thumbnail } pendingRequests[cacheKey] = task return await task.value } func clearCache() { cache.removeAll() } func clearCache(for url: URL) { cache = cache.filter { !$0.key.starts(with: url.path) } } // MARK: - Private Methods private func generateQuickLookThumbnail(for url: URL, size: CGSize) async -> NSImage? { let scale = await MainActor.run { NSScreen.main?.backingScaleFactor ?? 2.0 } return await url.accessSecurityScopedResource { scopedURL in NSLog("🔐 ThumbnailService: obtaining security scope for \(scopedURL.path)") let request = QLThumbnailGenerator.Request( fileAt: scopedURL, size: size, scale: scale, representationTypes: .all ) request.iconMode = true return await withCheckedContinuation { (continuation: CheckedContinuation) in thumbnailGenerator.generateBestRepresentation(for: request) { representation, error in if let rep = representation { NSLog("🔍 ThumbnailService: generated thumbnail for \(scopedURL.path)") continuation.resume(returning: rep.nsImage) } else { if let err = error { NSLog("⚠️ ThumbnailService: thumbnail error for \(scopedURL.path): \(err.localizedDescription)") } continuation.resume(returning: nil) } } } } } } // MARK: - Extensions extension QLThumbnailRepresentation { var nsImage: NSImage { return NSImage(cgImage: self.cgImage, size: self.cgImage.size) } } extension CGImage { var size: NSSize { return NSSize(width: self.width, height: self.height) } } ================================================ FILE: boringNotch/components/Shelf/ViewModels/ShelfItemViewModel.swift ================================================ // // ShelfItemViewModel.swift // boringNotch // // Created by Alexander on 2025-09-24. // import Foundation import AppKit import SwiftUI import UniformTypeIdentifiers import CoreServices import ObjectiveC @MainActor final class ShelfItemViewModel: ObservableObject { @Published private(set) var item: ShelfItem @Published var thumbnail: NSImage? @Published var isDropTargeted: Bool = false @Published var isRenaming: Bool = false @Published var draftTitle: String = "" private var sharingLifecycle: SharingLifecycleDelegate? private var quickShareLifecycle: SharingLifecycleDelegate? private var sharingAccessingURLs: [URL] = [] private static var copiedURLs: [URL] = [] private let selection = ShelfSelectionModel.shared init(item: ShelfItem) { self.item = item self.draftTitle = item.displayName Task { await loadThumbnail() } } var isSelected: Bool { selection.isSelected(item.id) } func loadThumbnail() async { guard let url = item.fileURL else { return } if let image = await ThumbnailService.shared.thumbnail(for: url, size: CGSize(width: 56, height: 56)) { self.thumbnail = image } } // MARK: - Drag & Drop helpers func dragItemProvider() -> NSItemProvider { let selectedItems = selection.selectedItems(in: ShelfStateViewModel.shared.items) if selectedItems.count > 1 && selectedItems.contains(where: { $0.id == item.id }) { return createMultiItemProvider(for: selectedItems) } return createItemProvider(for: item) } private func createItemProvider(for item: ShelfItem) -> NSItemProvider { switch item.kind { case .file: let provider = NSItemProvider() if let url = ShelfStateViewModel.shared.resolveAndUpdateBookmark(for: item) { provider.registerObject(url as NSURL, visibility: .all) } else { provider.registerObject(item.displayName as NSString, visibility: .all) } return provider case .text(let string): return NSItemProvider(object: string as NSString) case .link(let url): return NSItemProvider(object: url as NSURL) } } private func createMultiItemProvider(for items: [ShelfItem]) -> NSItemProvider { let provider = NSItemProvider() var urls: [URL] = [] var textItems: [String] = [] for item in items { switch item.kind { case .file: if let url = ShelfStateViewModel.shared.resolveAndUpdateBookmark(for: item) { urls.append(url) } else { textItems.append(item.displayName) } case .text(let string): textItems.append(string) case .link: break } } if !urls.isEmpty { for url in urls { provider.registerObject(url as NSURL, visibility: .all) } } if !textItems.isEmpty { provider.registerObject(textItems.joined(separator: "\n") as NSString, visibility: .all) } return provider } // MARK: - Actions func handleClick(event: NSEvent, view: NSView) { let flags = event.modifierFlags if flags.contains(.shift) { selection.shiftSelect(to: item, in: ShelfStateViewModel.shared.items) } else if flags.contains(.command) { selection.toggle(item) } else if flags.contains(.control) { handleRightClick(event: event, view: view) } else { if !selection.isSelected(item.id) { selection.selectSingle(item) } } if event.clickCount == 2 { handleDoubleClick() } } func handleRightClick(event: NSEvent, view: NSView) { if !selection.isSelected(item.id) { selection.selectSingle(item) } presentContextMenu(event: event, in: view) } func handleDoubleClick() { let selected = ShelfSelectionModel.shared.selectedItems(in: ShelfStateViewModel.shared.items) for it in selected { ShelfActionService.open(it) } } func shareItem(from view: NSView?) { Task { var itemsToShare: [Any] = [] var fileURLs: [URL] = [] if case .text(let text) = item.kind { itemsToShare.append(text) } else { for item in ShelfSelectionModel.shared.selectedItems(in: ShelfStateViewModel.shared.items) { switch item.kind { case .file: // Use immediate update for user-initiated share action if let url = ShelfStateViewModel.shared.resolveAndUpdateBookmark(for: item) { itemsToShare.append(url) fileURLs.append(url) } case .text(let string): itemsToShare.append(string) case .link(let url): itemsToShare.append(url) } } } guard !itemsToShare.isEmpty else { return } stopSharingAccessingURLs() // Start security-scoped access for all file URLs and keep it active during sharing sharingAccessingURLs = fileURLs.filter { $0.startAccessingSecurityScopedResource() } // Create and retain lifecycle delegate for the entire share operation let lifecycle = SharingStateManager.shared.makeDelegate { [weak self] in self?.sharingLifecycle = nil self?.stopSharingAccessingURLs() } self.sharingLifecycle = lifecycle let picker = NSSharingServicePicker(items: itemsToShare) picker.delegate = lifecycle lifecycle.markPickerBegan() if let view { picker.show(relativeTo: .zero, of: view, preferredEdge: .minY) } } } private func stopSharingAccessingURLs() { for url in sharingAccessingURLs { url.stopAccessingSecurityScopedResource() } sharingAccessingURLs.removeAll() } /// Call this closure to request a QuickLook preview for the given URLs. var onQuickLookRequest: (([URL]) -> Void)? // MARK: - Context Menu helpers (extracted from view) func loadOpenWithApps() -> [URL] { // Support both files and link items. For link items we ask NSWorkspace for apps that can open the URL (browsers). if let fileURL = item.fileURL { var results: [URL] = NSWorkspace.shared.urlsForApplications(toOpen: fileURL) if results.isEmpty { if let uti = try? fileURL.resourceValues(forKeys: [.contentTypeKey]).contentType { results = NSWorkspace.shared.urlsForApplications(toOpen: uti) } } let unique = Array(Set(results)) let sorted = unique.sorted { appDisplayName(for: $0) < appDisplayName(for: $1) } return sorted } else if case .link(let url) = item.kind { var results: [URL] = NSWorkspace.shared.urlsForApplications(toOpen: url) if results.isEmpty { if let uti = try? url.resourceValues(forKeys: [.contentTypeKey]).contentType { results = NSWorkspace.shared.urlsForApplications(toOpen: uti) } } let unique = Array(Set(results)) let sorted = unique.sorted { appDisplayName(for: $0) < appDisplayName(for: $1) } return sorted } return [] } private func ensureContextMenuSelection() { if !selection.isSelected(item.id) { selection.selectSingle(item) } } func presentContextMenu(event: NSEvent, in view: NSView) { ensureContextMenuSelection() let menu = NSMenu() func addMenuItem(title: String) { let mi = NSMenuItem(title: title, action: nil, keyEquivalent: "") menu.addItem(mi) } let selectedItems = ShelfSelectionModel.shared.selectedItems(in: ShelfStateViewModel.shared.items) let selectedFileURLs = selectedItems.compactMap { $0.fileURL } let selectedLinkURLs: [URL] = selectedItems.compactMap { itm in if case .link(let url) = itm.kind { return url } return nil } let selectedFolderURLs = selectedFileURLs.filter { isDirectory($0) } // URLs valid for Open/Open With (exclude folders) let selectedOpenableURLs = selectedItems.compactMap { itm -> URL? in if let u = itm.fileURL { return isDirectory(u) ? nil : u } if case .link(let url) = itm.kind { return url } return nil } if !selectedOpenableURLs.isEmpty { addMenuItem(title: "Open") } if !selectedOpenableURLs.isEmpty { let openWith = NSMenuItem(title: "Open With", action: nil, keyEquivalent: "") let submenu = NSMenu() // Choose a representative URL to compute apps (prefer current item if not a folder) let baseURLForApps: URL? = { if let u = item.fileURL, !isDirectory(u) { return u } if case .link(let u) = item.kind { return u } return selectedOpenableURLs.first }() let openWithApps: [URL] = { guard let u = baseURLForApps else { return [] } if u.isFileURL { var results = NSWorkspace.shared.urlsForApplications(toOpen: u) if results.isEmpty, let uti = try? u.resourceValues(forKeys: [.contentTypeKey]).contentType { results = NSWorkspace.shared.urlsForApplications(toOpen: uti) } return Array(Set(results)) } else { return Array(Set(NSWorkspace.shared.urlsForApplications(toOpen: u))) } }() let defaultApp = defaultAppURL() if openWithApps.isEmpty { let noApps = NSMenuItem(title: "No Compatible Apps Found", action: nil, keyEquivalent: "") noApps.isEnabled = false submenu.addItem(noApps) } else { if let defaultApp = defaultApp { let appName = appDisplayName(for: defaultApp) let def = NSMenuItem(title: appName, action: nil, keyEquivalent: "") def.representedObject = defaultApp def.image = nsAppIcon(for: defaultApp, size: 16) let title = NSMutableAttributedString(string: appName, attributes: [ .font: NSFont.menuFont(ofSize: 0), .foregroundColor: NSColor.labelColor ]) let defaultPart = NSAttributedString(string: " (default)", attributes: [ .font: NSFont.menuFont(ofSize: 0), .foregroundColor: NSColor.secondaryLabelColor ]) title.append(defaultPart) def.attributedTitle = title submenu.addItem(def) if openWithApps.count > 1 || !openWithApps.contains(defaultApp) { submenu.addItem(NSMenuItem.separator()) } } for appURL in openWithApps where appURL != defaultApp { let mi = NSMenuItem(title: appDisplayName(for: appURL), action: nil, keyEquivalent: "") mi.representedObject = appURL mi.image = nsAppIcon(for: appURL, size: 16) submenu.addItem(mi) } } submenu.addItem(NSMenuItem.separator()) let other = NSMenuItem(title: "Other…", action: nil, keyEquivalent: "") other.representedObject = "__OTHER__" submenu.addItem(other) openWith.submenu = submenu menu.addItem(openWith) } if !selectedFileURLs.isEmpty { addMenuItem(title: "Show in Finder") } // Allow Quick Look for files and link URLs if !selectedFileURLs.isEmpty || !selectedLinkURLs.isEmpty { // Add Quick Look menu item let quickLookItem = NSMenuItem(title: "Quick Look", action: nil, keyEquivalent: "") menu.addItem(quickLookItem) // Add Slideshow as alternate menu item (shown when Option key is held) let slideshowItem = NSMenuItem(title: "Quick Look", action: nil, keyEquivalent: "") slideshowItem.isAlternate = true slideshowItem.keyEquivalentModifierMask = [.option] menu.addItem(slideshowItem) } menu.addItem(NSMenuItem.separator()) addMenuItem(title: "Share…") // Add image processing options for image files grouped under "Image Actions" let imageURLs = selectedFileURLs.filter { ImageProcessingService.shared.isImageFile($0) } if !imageURLs.isEmpty { menu.addItem(NSMenuItem.separator()) let imageActions = NSMenuItem(title: "Image Actions", action: nil, keyEquivalent: "") let imageSubmenu = NSMenu() // Remove Background - only for single images if imageURLs.count == 1 { let removeBg = NSMenuItem(title: "Remove Background", action: nil, keyEquivalent: "") imageSubmenu.addItem(removeBg) } // Convert Image - only for single images if imageURLs.count == 1 { let convertItem = NSMenuItem(title: "Convert Image…", action: nil, keyEquivalent: "") imageSubmenu.addItem(convertItem) } // Create PDF - for one or more images let createPDF = NSMenuItem(title: "Create PDF", action: nil, keyEquivalent: "") imageSubmenu.addItem(createPDF) imageActions.submenu = imageSubmenu menu.addItem(imageActions) menu.addItem(NSMenuItem.separator()) } // Add compression option for files/folders (single or multiple) if !selectedFileURLs.isEmpty { let compressItem = NSMenuItem(title: "Compress", action: nil, keyEquivalent: "") menu.addItem(compressItem) } if selectedItems.count == 1, case .file(_) = item.kind { addMenuItem(title: "Rename") } // Always show "Copy" for all item types addMenuItem(title: "Copy") // If there are file URLs, add "Copy Path" as an alternate menu item (Option key) if !selectedFileURLs.isEmpty { let copyPathItem = NSMenuItem(title: "Copy Path", action: nil, keyEquivalent: "") copyPathItem.isAlternate = true copyPathItem.keyEquivalentModifierMask = [.option] menu.addItem(copyPathItem) } menu.addItem(NSMenuItem.separator()) addMenuItem(title: "Remove") let actionTarget = MenuActionTarget(item: item, view: view, viewModel: self) for menuItem in menu.items { if menuItem.isSeparatorItem { continue } menuItem.target = actionTarget menuItem.action = #selector(MenuActionTarget.handle(_:)) if let submenu = menuItem.submenu { for subItem in submenu.items { if !subItem.isSeparatorItem { subItem.target = actionTarget subItem.action = #selector(MenuActionTarget.handle(_:)) } } } } menu.retainActionTarget(actionTarget) NSMenu.popUpContextMenu(menu, with: event, for: view) } private func isDirectory(_ url: URL) -> Bool { return url.accessSecurityScopedResource { scoped in (try? scoped.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false } } private final class MenuActionTarget: NSObject { let item: ShelfItem weak var view: NSView? unowned let viewModel: ShelfItemViewModel // Keep associated objects (like accessory view handlers) without magic keys private static var sliderHandlerAssoc = AssociatedObject() init(item: ShelfItem, view: NSView, viewModel: ShelfItemViewModel) { self.item = item self.view = view self.viewModel = viewModel } @MainActor @objc func handle(_ sender: NSMenuItem) { let title = sender.title if let marker = sender.representedObject as? String, marker == "__OTHER__" { openWithPanel() return } if let appURL = sender.representedObject as? URL { let selected = ShelfSelectionModel.shared.selectedItems(in: ShelfStateViewModel.shared.items) Task { var allSelectedURLs: [URL] = [] for itm in selected { if let fileURL = itm.fileURL { allSelectedURLs.append(fileURL) } else if case .link(let url) = itm.kind { allSelectedURLs.append(url) } } guard !allSelectedURLs.isEmpty else { return } let config = NSWorkspace.OpenConfiguration() let fileURLs = allSelectedURLs.filter { $0.isFileURL } do { if !fileURLs.isEmpty { _ = try await fileURLs.accessSecurityScopedResources { _ in try await NSWorkspace.shared.open(allSelectedURLs, withApplicationAt: appURL, configuration: config) } } else { try await NSWorkspace.shared.open(allSelectedURLs, withApplicationAt: appURL, configuration: config) } } catch { print("❌ Failed to open with application: \(error.localizedDescription)") } } return } switch title { case "Quick Look": // Handle all selected items for Quick Look, not just the clicked item let selected = ShelfSelectionModel.shared.selectedItems(in: ShelfStateViewModel.shared.items) let urls: [URL] = selected.compactMap { item in if let fileURL = item.fileURL { return fileURL } if case .link(let url) = item.kind { return url } return nil } if !urls.isEmpty { viewModel.onQuickLookRequest?(urls) } case "Open": let selected = ShelfSelectionModel.shared.selectedItems(in: ShelfStateViewModel.shared.items) for it in selected { ShelfActionService.open(it) } case "Share…": viewModel.shareItem(from: view) case "Rename": let selected = ShelfSelectionModel.shared.selectedItems(in: ShelfStateViewModel.shared.items) if selected.count == 1, let single = selected.first { showRenameDialog(for: single) } case "Show in Finder": let selected = ShelfSelectionModel.shared.selectedItems(in: ShelfStateViewModel.shared.items) Task { let urls = await selected.asyncCompactMap { item -> URL? in if case .file = item.kind { // Use immediate update for user-initiated menu action return await ShelfStateViewModel.shared.resolveAndUpdateBookmark(for: item) } return nil } if !urls.isEmpty { await urls.accessSecurityScopedResources { accessibleURLs in NSWorkspace.shared.activateFileViewerSelecting(accessibleURLs) } } } case "Copy Path": let selected = ShelfSelectionModel.shared.selectedItems(in: ShelfStateViewModel.shared.items) let paths = selected.compactMap { $0.fileURL?.path } if !paths.isEmpty { NSPasteboard.general.clearContents() NSPasteboard.general.setString(paths.joined(separator: "\n"), forType: .string) } case "Copy": let selected = ShelfSelectionModel.shared.selectedItems(in: ShelfStateViewModel.shared.items) let pb = NSPasteboard.general // Stop accessing previously copied URLs for url in ShelfItemViewModel.copiedURLs { url.stopAccessingSecurityScopedResource() } ShelfItemViewModel.copiedURLs.removeAll() pb.clearContents() Task { let fileURLs = await selected.asyncCompactMap { item -> URL? in if case .file = item.kind { return ShelfStateViewModel.shared.resolveAndUpdateBookmark(for: item) } return nil } if !fileURLs.isEmpty { // Start security-scoped access for all URLs and keep them active ShelfItemViewModel.copiedURLs = fileURLs.filter { $0.startAccessingSecurityScopedResource() } NSLog("🔐 Started security-scoped access for \(ShelfItemViewModel.copiedURLs.count) copied files") // Write to pasteboard pb.writeObjects(fileURLs as [NSURL]) } else { let strings = selected.map { $0.displayName } if !strings.isEmpty { pb.setString(strings.joined(separator: "\n"), forType: .string) } } } case "Remove": let selected = ShelfSelectionModel.shared.selectedItems(in: ShelfStateViewModel.shared.items) for it in selected { ShelfActionService.remove(it) } case "Remove Background": handleRemoveBackground() case "Convert Image…": showConvertImageDialog() case "Create PDF": handleCreatePDF() case "Compress": let selected = ShelfSelectionModel.shared.selectedItems(in: ShelfStateViewModel.shared.items) let fileURLs = selected.compactMap { $0.fileURL } guard !fileURLs.isEmpty else { break } Task { do { // Create ZIP in a temporary location while holding access to selected resources if let zipTempURL = try await fileURLs.accessSecurityScopedResources(accessor: { urls in await TemporaryFileStorageService.shared.createZip(from: urls) }) { if let bookmark = try? Bookmark(url: zipTempURL) { let newItem = ShelfItem(kind: .file(bookmark: bookmark.data), isTemporary: true) ShelfStateViewModel.shared.add([newItem]) } else { // Fallback: reveal the temporary file in Finder NSWorkspace.shared.activateFileViewerSelecting([zipTempURL]) } } } catch { print("❌ Compress failed: \(error)") } } default: break } } @MainActor private func openWithPanel() { // Support both file items and link items let targetURL: URL? let needsSecurityScope: Bool if let fileURL = item.fileURL { targetURL = fileURL needsSecurityScope = true } else if case .link(let url) = item.kind { targetURL = url needsSecurityScope = false } else { targetURL = nil needsSecurityScope = false } guard let fileURL = targetURL else { return } let panel = NSOpenPanel() panel.title = "Choose Application" panel.message = "Choose an application to open the document \"\(item.displayName)\"." panel.prompt = "Open" panel.allowsMultipleSelection = false panel.canChooseFiles = true panel.canChooseDirectories = false panel.resolvesAliases = true if #available(macOS 12.0, *) { panel.allowedContentTypes = [.application] } panel.directoryURL = URL(fileURLWithPath: "/Applications") // Compute recommended applications for the selected target let recommendedApps: Set = { let apps: [URL] if let uti = (try? fileURL.resourceValues(forKeys: [.contentTypeKey]))?.contentType { apps = NSWorkspace.shared.urlsForApplications(toOpen: uti) } else { apps = NSWorkspace.shared.urlsForApplications(toOpen: fileURL) } return Set(apps.map { $0.standardizedFileURL }) }() // Delegate to filter entries when in "Recommended Applications" mode final class AppChooserDelegate: NSObject, NSOpenSavePanelDelegate { enum Mode { case recommended, all } var mode: Mode = .recommended let recommended: Set init(recommended: Set) { self.recommended = recommended } func panel(_ sender: Any, shouldEnable url: URL) -> Bool { let ext = url.pathExtension.lowercased() if ext == "app" { switch mode { case .all: return true case .recommended: // Standardize URLs for reliable comparison let std = url.standardizedFileURL return recommended.contains(std) } } var isDirectory: ObjCBool = false if FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory), isDirectory.boolValue { return true } return false } } let chooserDelegate = AppChooserDelegate(recommended: recommendedApps) panel.delegate = chooserDelegate let enableLabel = NSTextField(labelWithString: "Enable:") enableLabel.font = .systemFont(ofSize: NSFont.systemFontSize) enableLabel.alignment = .natural enableLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal) let popup = NSPopUpButton(frame: .zero, pullsDown: false) popup.addItems(withTitles: ["Recommended Applications", "All Applications"]) popup.font = .systemFont(ofSize: NSFont.systemFontSize) popup.selectItem(at: 0) popup.setContentHuggingPriority(.defaultLow, for: .horizontal) popup.widthAnchor.constraint(greaterThanOrEqualToConstant: 200).isActive = true let alwaysCheckbox = NSButton(checkboxWithTitle: "Always Open With", target: nil, action: nil) alwaysCheckbox.font = .systemFont(ofSize: NSFont.systemFontSize) alwaysCheckbox.setContentHuggingPriority(.defaultLow, for: .horizontal) let row = NSStackView(views: [enableLabel, popup]) row.orientation = .horizontal row.spacing = 8 row.alignment = .centerY row.distribution = .fill let column = NSStackView(views: [row, alwaysCheckbox]) column.orientation = .vertical column.spacing = 12 column.alignment = .centerX column.distribution = .fill column.edgeInsets = NSEdgeInsets(top: 16, left: 20, bottom: 16, right: 20) panel.accessoryView = column panel.isAccessoryViewDisclosed = true // Wire up popup to switch filter mode class PopupBinder: NSObject { weak var popup: NSPopUpButton? weak var chooserDelegate: AppChooserDelegate? weak var panel: NSOpenPanel? init(popup: NSPopUpButton, chooserDelegate: AppChooserDelegate, panel: NSOpenPanel) { self.popup = popup self.chooserDelegate = chooserDelegate self.panel = panel } @objc func changed(_ sender: Any?) { if popup?.indexOfSelectedItem == 1 { chooserDelegate?.mode = .all } else { chooserDelegate?.mode = .recommended } if let panel = panel { panel.validateVisibleColumns() let currentDir = panel.directoryURL panel.directoryURL = currentDir } } } let binder = PopupBinder(popup: popup, chooserDelegate: chooserDelegate, panel: panel) popup.target = binder popup.action = #selector(PopupBinder.changed(_:)) panel.begin { response in if response == .OK, let appURL = panel.url { Task { do { let config = NSWorkspace.OpenConfiguration() if alwaysCheckbox.state == .on, let bundleID = Bundle(url: appURL)?.bundleIdentifier { if let contentType = (try? fileURL.resourceValues(forKeys: [.contentTypeKey]))?.contentType { let status = LSSetDefaultRoleHandlerForContentType(contentType.identifier as CFString, LSRolesMask.all, bundleID as CFString) if status != noErr { print("⚠️ Failed to set default handler for \(contentType.identifier): \(status)") } } else if let scheme = fileURL.scheme { let status = LSSetDefaultHandlerForURLScheme(scheme as CFString, bundleID as CFString) if status != noErr { print("⚠️ Failed to set default handler for scheme \(scheme): \(status)") } } } if needsSecurityScope { _ = try await fileURL.accessSecurityScopedResource { accessibleURL in try await NSWorkspace.shared.open([accessibleURL], withApplicationAt: appURL, configuration: config) } } else { try await NSWorkspace.shared.open([fileURL], withApplicationAt: appURL, configuration: config) } } catch { print("❌ Failed to open with application: \(error.localizedDescription)") } } } // Keep binder/delegate alive until panel finishes _ = binder _ = chooserDelegate } } @MainActor private func showRenameDialog(for item: ShelfItem) { guard case let .file(bookmarkData) = item.kind else { return } Task { let bookmark = Bookmark(data: bookmarkData) if let fileURL = bookmark.resolveURL() { // Start security-scoped access and keep it active until rename completes. let didStart = fileURL.startAccessingSecurityScopedResource() let savePanel = NSSavePanel() savePanel.title = "Rename File" savePanel.prompt = "Rename" savePanel.nameFieldStringValue = fileURL.lastPathComponent savePanel.directoryURL = fileURL.deletingLastPathComponent() savePanel.begin { response in if response == .OK, let newURL = savePanel.url { Task { do { NSLog("🔐 Rename: moving from \(fileURL.path) to \(newURL.path) (securityScope=\(didStart))") try FileManager.default.moveItem(at: fileURL, to: newURL) if let newBookmark = try? Bookmark(url: newURL) { ShelfStateViewModel.shared.updateBookmark(for: item, bookmark: newBookmark.data) } } catch { print("❌ Failed to rename file: \(error.localizedDescription)") } if didStart { fileURL.stopAccessingSecurityScopedResource() } } } else { if didStart { fileURL.stopAccessingSecurityScopedResource() } } } } } } @MainActor private func handleRemoveBackground() { let selected = ShelfSelectionModel.shared.selectedItems(in: ShelfStateViewModel.shared.items) let imageURLs = selected.compactMap { $0.fileURL }.filter { ImageProcessingService.shared.isImageFile($0) } guard let imageURL = imageURLs.first else { return } Task { do { let resultURL = try await imageURL.accessSecurityScopedResource { url in try await ImageProcessingService.shared.removeBackground(from: url) } if let resultURL = resultURL { // Create bookmark and add to shelf as temporary item if let bookmark = try? Bookmark(url: resultURL) { let newItem = ShelfItem( kind: .file(bookmark: bookmark.data), isTemporary: true ) ShelfStateViewModel.shared.add([newItem]) } } } catch { print("❌ Failed to remove background: \(error.localizedDescription)") await showErrorAlert(title: "Background Removal Failed", message: error.localizedDescription) } } } @MainActor private func handleCreatePDF() { let selected = ShelfSelectionModel.shared.selectedItems(in: ShelfStateViewModel.shared.items) let imageURLs = selected.compactMap { $0.fileURL }.filter { ImageProcessingService.shared.isImageFile($0) } guard !imageURLs.isEmpty else { return } Task { do { let resultURL = try await imageURLs.accessSecurityScopedResources { urls in try await ImageProcessingService.shared.createPDF(from: urls) } if let resultURL = resultURL { // Create bookmark and add to shelf as temporary item if let bookmark = try? Bookmark(url: resultURL) { let newItem = ShelfItem( kind: .file(bookmark: bookmark.data), isTemporary: true ) ShelfStateViewModel.shared.add([newItem]) } } } catch { print("❌ Failed to create PDF: \(error.localizedDescription)") await showErrorAlert(title: "PDF Creation Failed", message: error.localizedDescription) } } } @MainActor private func showConvertImageDialog() { let selected = ShelfSelectionModel.shared.selectedItems(in: ShelfStateViewModel.shared.items) let imageURLs = selected.compactMap { $0.fileURL }.filter { ImageProcessingService.shared.isImageFile($0) } guard let imageURL = imageURLs.first else { return } // Create and show conversion options dialog with better layout let alert = NSAlert() alert.messageText = "Convert Image" alert.alertStyle = .informational alert.addButton(withTitle: "Convert") alert.addButton(withTitle: "Cancel") // Create accessory view with better spacing and organization let accessoryView = NSView(frame: NSRect(x: 0, y: 0, width: 380, height: 180)) accessoryView.wantsLayer = true // MARK: Format Row let formatLabel = NSTextField(labelWithString: "Format:") formatLabel.frame = NSRect(x: 0, y: 145, width: 100, height: 20) formatLabel.font = .systemFont(ofSize: 12, weight: .medium) accessoryView.addSubview(formatLabel) let formatPopup = NSPopUpButton(frame: NSRect(x: 120, y: 140, width: 250, height: 28)) formatPopup.addItems(withTitles: ["PNG", "JPEG", "HEIC", "TIFF", "BMP"]) formatPopup.selectItem(at: 0) formatPopup.font = .systemFont(ofSize: 12) accessoryView.addSubview(formatPopup) // MARK: Image Size Row let imageSizeLabel = NSTextField(labelWithString: "Image Size:") imageSizeLabel.frame = NSRect(x: 0, y: 105, width: 100, height: 20) imageSizeLabel.font = .systemFont(ofSize: 12, weight: .medium) accessoryView.addSubview(imageSizeLabel) let imageSizePopup = NSPopUpButton(frame: NSRect(x: 120, y: 100, width: 160, height: 28)) imageSizePopup.addItems(withTitles: ["Actual Size", "Large", "Medium", "Small", "Custom..."]) imageSizePopup.selectItem(at: 0) imageSizePopup.font = .systemFont(ofSize: 12) accessoryView.addSubview(imageSizePopup) // Custom size field (initially hidden) let customSizeField = NSTextField(frame: NSRect(x: 285, y: 103, width: 85, height: 22)) customSizeField.placeholderString = "e.g., 1920" customSizeField.font = .systemFont(ofSize: 12) customSizeField.isHidden = true accessoryView.addSubview(customSizeField) // MARK: Preserve Metadata Checkbox let metadataCheckbox = NSButton(checkboxWithTitle: "Preserve Metadata", target: nil, action: nil) metadataCheckbox.frame = NSRect(x: 120, y: 65, width: 200, height: 20) metadataCheckbox.font = .systemFont(ofSize: 12) metadataCheckbox.state = .on accessoryView.addSubview(metadataCheckbox) // MARK: Separator line let separatorLine = NSView(frame: NSRect(x: 0, y: 50, width: 380, height: 1)) separatorLine.wantsLayer = true separatorLine.layer?.backgroundColor = NSColor.separatorColor.cgColor accessoryView.addSubview(separatorLine) // MARK: Format-specific options (shown/hidden based on format selection) let qualityRow = NSView(frame: NSRect(x: 0, y: 15, width: 380, height: 30)) qualityRow.wantsLayer = true let qualityLabel = NSTextField(labelWithString: "Compression:") qualityLabel.frame = NSRect(x: 0, y: 7, width: 100, height: 20) qualityLabel.font = .systemFont(ofSize: 12, weight: .medium) qualityRow.addSubview(qualityLabel) let qualitySlider = NSSlider(frame: NSRect(x: 120, y: 12, width: 200, height: 20)) qualitySlider.minValue = 0.0 qualitySlider.maxValue = 1.0 qualitySlider.doubleValue = 0.85 accessoryView.addSubview(qualitySlider) let qualityValueLabel = NSTextField(labelWithString: "85%") qualityValueLabel.frame = NSRect(x: 325, y: 7, width: 55, height: 20) qualityValueLabel.font = .systemFont(ofSize: 12) qualityValueLabel.alignment = .left accessoryView.addSubview(qualityValueLabel) // Update quality label and hide/show compression row based on format let updateQualityLabel = { let value = Int(qualitySlider.doubleValue * 100) qualityValueLabel.stringValue = "\(value)%" } let updateCompressionVisibility = { let formatIndex = formatPopup.indexOfSelectedItem let showCompression = formatIndex == 1 || formatIndex == 2 // JPEG or HEIC qualitySlider.isHidden = !showCompression qualityValueLabel.isHidden = !showCompression qualityLabel.isHidden = !showCompression } let updateCustomSizeVisibility = { let sizeIndex = imageSizePopup.indexOfSelectedItem customSizeField.isHidden = sizeIndex != 4 // Show only for "Custom..." } // Create a target object to handle slider value changes class SliderHandler: NSObject { let updateLabel: () -> Void let updateVisibility: () -> Void let updateCustomSize: () -> Void init(updateLabel: @escaping () -> Void, updateVisibility: @escaping () -> Void, updateCustomSize: @escaping () -> Void) { self.updateLabel = updateLabel self.updateVisibility = updateVisibility self.updateCustomSize = updateCustomSize } @objc func sliderChanged(_ sender: NSSlider) { updateLabel() } @objc func formatChanged(_ sender: NSPopUpButton) { updateVisibility() } @objc func sizeChanged(_ sender: NSPopUpButton) { updateCustomSize() } } let handler = SliderHandler(updateLabel: updateQualityLabel, updateVisibility: updateCompressionVisibility, updateCustomSize: updateCustomSizeVisibility) qualitySlider.target = handler qualitySlider.action = #selector(SliderHandler.sliderChanged(_:)) qualitySlider.isContinuous = true formatPopup.target = handler formatPopup.action = #selector(SliderHandler.formatChanged(_:)) imageSizePopup.target = handler imageSizePopup.action = #selector(SliderHandler.sizeChanged(_:)) updateCompressionVisibility() updateQualityLabel() updateCustomSizeVisibility() // Keep the handler alive using the `AssociatedObject` helper instead of a magic string key MenuActionTarget.sliderHandlerAssoc[accessoryView] = handler alert.accessoryView = accessoryView let response = alert.runModal() if response == .alertFirstButtonReturn { // Get selected options let formatIndex = formatPopup.indexOfSelectedItem let format: ImageConversionOptions.ImageFormat switch formatIndex { case 0: format = .png case 1: format = .jpeg case 2: format = .heic case 3: format = .tiff case 4: format = .bmp default: format = .png } let quality = qualitySlider.doubleValue // Get max dimension based on image size selection let maxDimension: CGFloat? = { let sizeIndex = imageSizePopup.indexOfSelectedItem switch sizeIndex { case 0: return nil // Actual Size case 1: return 1280 // Large case 2: return 640 // Medium case 3: return 320 // Small case 4: // Custom (user-specified) let text = customSizeField.stringValue.trimmingCharacters(in: .whitespaces) guard !text.isEmpty, let value = Double(text), value > 0 else { return nil } return CGFloat(value) default: return nil } }() let removeMetadata = metadataCheckbox.state == .off // Note: we invert this let options = ImageConversionOptions( format: format, compressionQuality: quality, maxDimension: maxDimension, removeMetadata: removeMetadata ) Task { do { let resultURL = try await imageURL.accessSecurityScopedResource { url in try await ImageProcessingService.shared.convertImage(from: url, options: options) } if let resultURL = resultURL { // Create bookmark and add to shelf as temporary item if let bookmark = try? Bookmark(url: resultURL) { let newItem = ShelfItem( kind: .file(bookmark: bookmark.data), isTemporary: true ) ShelfStateViewModel.shared.add([newItem]) } } } catch { print("❌ Failed to convert image: \(error.localizedDescription)") showErrorAlert(title: "Image Conversion Failed", message: error.localizedDescription) } } } } @MainActor private func showErrorAlert(title: String, message: String) { let alert = NSAlert() alert.messageText = title alert.informativeText = message alert.alertStyle = .warning alert.addButton(withTitle: "OK") alert.runModal() } } // MARK: - Private helpers private func appDisplayName(for appURL: URL) -> String { (try? appURL.resourceValues(forKeys: [.localizedNameKey]).localizedName) ?? appURL.lastPathComponent } private func nsAppIcon(for appURL: URL, size: CGFloat) -> NSImage? { let baseIcon = NSWorkspace.shared.icon(forFile: appURL.path) baseIcon.isTemplate = false let targetSize = NSSize(width: size, height: size) let rendered = NSImage(size: targetSize, flipped: false) { rect in NSGraphicsContext.current?.imageInterpolation = .high baseIcon.draw(in: rect, from: .zero, operation: .sourceOver, fraction: 1.0, respectFlipped: true, hints: [ .interpolation: NSImageInterpolation.high.rawValue ]) return true } rendered.size = targetSize return rendered } private func defaultAppURL() -> URL? { if let fileURL = item.fileURL { return NSWorkspace.shared.urlForApplication(toOpen: fileURL) } else if case .link(let url) = item.kind { return NSWorkspace.shared.urlForApplication(toOpen: url) } return nil } } fileprivate extension Sequence { func asyncCompactMap(_ transform: (Element) async -> T?) async -> [T] { var result: [T] = [] for element in self { if let transformed = await transform(element) { result.append(transformed) } } return result } } ================================================ FILE: boringNotch/components/Shelf/ViewModels/ShelfSelectionModel.swift ================================================ // // ShelfSelectionModel.swift // boringNotch // // Created by Alexander on 2025-09-26. // import Foundation import Combine private let _shelfTypeAnchor: Bool = { _ = String(describing: ShelfItem.self) return true }() @MainActor final class ShelfSelectionModel: ObservableObject { static let shared = ShelfSelectionModel() @Published private(set) var selectedIDs: Set = [] // Anchor for shift-range selection private var lastAnchorID: UUID? = nil func isSelected(_ id: UUID) -> Bool { selectedIDs.contains(id) } var hasSelection: Bool { !selectedIDs.isEmpty } var firstSelectedItem: ShelfItem? { guard let firstID = selectedIDs.first else { return nil } return ShelfStateViewModel.shared.items.first(where: { $0.id == firstID }) } func selectedItems(in allItems: [ShelfItem]) -> [ShelfItem] { allItems.filter { selectedIDs.contains($0.id) } } func selectSingle(_ item: ShelfItem) { selectedIDs = [item.id] lastAnchorID = item.id } func toggle(_ item: ShelfItem) { if selectedIDs.contains(item.id) { selectedIDs.remove(item.id) } else { selectedIDs.insert(item.id) } lastAnchorID = item.id } func shiftSelect(to item: ShelfItem, in allItems: [ShelfItem]) { // Determine anchor let anchorID = lastAnchorID ?? selectedIDs.first ?? item.id guard let startIndex = allItems.firstIndex(where: { $0.id == anchorID }), let endIndex = allItems.firstIndex(where: { $0.id == item.id }) else { // Fallback to single select if indices not found return selectSingle(item) } let lower = min(startIndex, endIndex) let upper = max(startIndex, endIndex) let rangeIDs = allItems[lower...upper].map { $0.id } selectedIDs = Set(rangeIDs) } func clear() { selectedIDs.removeAll() lastAnchorID = nil } // Keep anchor sane if items array changed drastically (optional helper) func ensureValidAnchor(in allItems: [ShelfItem]) { if let anchor = lastAnchorID, !allItems.contains(where: { $0.id == anchor }) { lastAnchorID = selectedIDs.first } } @Published private(set) var isDragging: Bool = false func beginDrag() { isDragging = true } func endDrag() { isDragging = false } } ================================================ FILE: boringNotch/components/Shelf/ViewModels/ShelfStateViewModel.swift ================================================ // // ShelfStateViewModel.swift // boringNotch // // Created by Alexander on 2025-10-09. import Foundation import AppKit @MainActor final class ShelfStateViewModel: ObservableObject { static let shared = ShelfStateViewModel() @Published private(set) var items: [ShelfItem] = [] { didSet { ShelfPersistenceService.shared.save(items) } } @Published var isLoading: Bool = false var isEmpty: Bool { items.isEmpty } // Queue for deferred bookmark updates to avoid publishing during view updates private var pendingBookmarkUpdates: [ShelfItem.ID: Data] = [:] private var updateTask: Task? private init() { items = ShelfPersistenceService.shared.load() } func add(_ newItems: [ShelfItem]) { guard !newItems.isEmpty else { return } var merged = items // Deduplicate by identityKey while preserving order (existing first) var seen: Set = Set(merged.map { $0.identityKey }) for it in newItems { let key = it.identityKey if !seen.contains(key) { merged.append(it) seen.insert(key) } } items = merged } func remove(_ item: ShelfItem) { item.cleanupStoredData() items.removeAll { $0.id == item.id } } func updateBookmark(for item: ShelfItem, bookmark: Data) { guard let idx = items.firstIndex(where: { $0.id == item.id }) else { return } if case .file = items[idx].kind { items[idx].kind = .file(bookmark: bookmark) } } private func scheduleDeferredBookmarkUpdate(for item: ShelfItem, bookmark: Data) { pendingBookmarkUpdates[item.id] = bookmark // Cancel existing task and schedule a new one updateTask?.cancel() updateTask = Task { @MainActor [weak self] in await Task.yield() guard let self = self else { return } for (itemID, bookmarkData) in self.pendingBookmarkUpdates { if let idx = self.items.firstIndex(where: { $0.id == itemID }), case .file = self.items[idx].kind { self.items[idx].kind = .file(bookmark: bookmarkData) } } self.pendingBookmarkUpdates.removeAll() } } func load(_ providers: [NSItemProvider]) { guard !providers.isEmpty else { return } isLoading = true Task { [weak self] in let dropped = await ShelfDropService.items(from: providers) await MainActor.run { self?.add(dropped) self?.isLoading = false } } } func cleanupInvalidItems() { Task { [weak self] in guard let self else { return } var keep: [ShelfItem] = [] for item in self.items { switch item.kind { case .file(let data): let bookmark = Bookmark(data: data) if await bookmark.validate() { keep.append(item) } else { item.cleanupStoredData() } default: keep.append(item) } } await MainActor.run { self.items = keep } } } func resolveFileURL(for item: ShelfItem) -> URL? { guard case .file(let bookmarkData) = item.kind else { return nil } let bookmark = Bookmark(data: bookmarkData) let result = bookmark.resolve() if let refreshed = result.refreshedData, refreshed != bookmarkData { NSLog("Bookmark for \(item) stale; refreshing") scheduleDeferredBookmarkUpdate(for: item, bookmark: refreshed) } return result.url } func resolveAndUpdateBookmark(for item: ShelfItem) -> URL? { guard case .file(let bookmarkData) = item.kind else { return nil } let bookmark = Bookmark(data: bookmarkData) let result = bookmark.resolve() if let refreshed = result.refreshedData, refreshed != bookmarkData { NSLog("Bookmark for \(item) stale; refreshing") updateBookmark(for: item, bookmark: refreshed) } return result.url } func resolveFileURLs(for items: [ShelfItem]) -> [URL] { var urls: [URL] = [] for it in items { if let u = resolveFileURL(for: it) { urls.append(u) } } return urls } } ================================================ FILE: boringNotch/components/Shelf/Views/DragPreviewView.swift ================================================ import SwiftUI import AppKit struct DragPreviewView: View { let thumbnail: NSImage? let displayName: String var body: some View { VStack(alignment: .center, spacing: 4) { Image(nsImage: thumbnail ?? NSImage()) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 56, height: 56) .clipShape(RoundedRectangle(cornerRadius: 12)) Text(displayName) .font(.system(size: 12, weight: .medium)) .foregroundColor(.white) .lineLimit(2) .truncationMode(.middle) .multilineTextAlignment(.center) .padding(.horizontal, 8) .padding(.vertical, 2) .background(RoundedRectangle(cornerRadius: 4).fill(Color.accentColor)) .frame(alignment: .top) } .frame(width: 105) } } ================================================ FILE: boringNotch/components/Shelf/Views/FileShareView.swift ================================================ // // FileShareView.swift // boringNotch // // Created by Alexander on 2025-09-24. // import AppKit import Defaults import SwiftUI import UniformTypeIdentifiers struct FileShareView: View { @EnvironmentObject private var vm: BoringViewModel @StateObject private var quickShare = QuickShareService.shared @Default(.quickShareProvider) var quickShareProvider: String @State private var hostView: NSView? @State private var interactionNonce: UUID = .init() @State private var isProcessing = false private var selectedProvider: QuickShareProvider { quickShare.availableProviders.first(where: { $0.id == quickShareProvider }) ?? QuickShareProvider(id: "System Share Menu", imageData: nil, supportsRawText: true) } var body: some View { dropArea .background(NSViewHost(view: $hostView)) .onDrop(of: [.fileURL, .url, .utf8PlainText, .plainText, .data, .image], isTargeted: $vm.dropZoneTargeting) { providers in interactionNonce = .init() vm.dropEvent = true Task { await handleDrop(providers) } return true } .onTapGesture { Task { await handleClick() } } } private var dropArea: some View { ZStack { RoundedRectangle(cornerRadius: 12) .fill( LinearGradient(colors: [Color.black.opacity(0.35), Color.black.opacity(0.20)], startPoint: .topLeading, endPoint: .bottomTrailing) ) .overlay( RoundedRectangle(cornerRadius: 12) .stroke( vm.dropZoneTargeting ? Color.accentColor.opacity(0.9) : Color.white.opacity(0.1), style: StrokeStyle(lineWidth: 3, lineCap: .round, dash: [10]) ) ) .shadow(color: Color.black.opacity(0.6), radius: 6, x: 0, y: 2) // Content VStack(spacing: 5) { ZStack { Circle() .fill(Color.white.opacity( vm.dropZoneTargeting ? 0.11 : 0.09 )) .frame(width: 55, height: 55) Image(systemName: "square.and.arrow.up") Group { if let imgData = selectedProvider.imageData, let nsImg = NSImage(data: imgData) { Image(nsImage: nsImg) .resizable() .aspectRatio(contentMode: .fit) } else { Image(systemName: "square.and.arrow.up") } } .frame(width: 34, height: 34) .foregroundStyle( vm.dropZoneTargeting ? Color.accentColor : Color.gray ) .scaleEffect( vm.dropZoneTargeting ? 1.06 : 1.0 ) .animation(.spring(response: 0.36, dampingFraction: 0.7), value: vm.dropZoneTargeting) } Text(selectedProvider.id) .font(.system(.headline, design: .rounded)) .foregroundColor(.white.opacity(0.8)) } .padding(18) // Loading overlay if isProcessing || quickShare.isPickerOpen { RoundedRectangle(cornerRadius: 12) .fill(.black.opacity(0.3)) .overlay( ProgressView() .progressViewStyle(CircularProgressViewStyle(tint: .white)) .scaleEffect(0.8) ) } } .contentShape(RoundedRectangle(cornerRadius: 12)) } // MARK: - Actions private func handleDrop(_ providers: [NSItemProvider]) async { isProcessing = true defer { isProcessing = false } await quickShare.shareDroppedFiles(providers, using: selectedProvider, from: hostView) } private func handleClick() async { await quickShare.showFilePicker(for: selectedProvider, from: hostView) } } // MARK: - Host NSView extractor for anchoring share sheet private struct NSViewHost: NSViewRepresentable { @Binding var view: NSView? func makeNSView(context: Context) -> NSView { let v = NSView(frame: .zero) DispatchQueue.main.async { self.view = v } return v } func updateNSView(_ nsView: NSView, context: Context) { DispatchQueue.main.async { self.view = nsView } } } ================================================ FILE: boringNotch/components/Shelf/Views/ShelfItemView.swift ================================================ // // ShelfItemView.swift // boringNotch // // Created by Alexander on 2025-09-24. // import AppKit import SwiftUI import Defaults import QuickLook struct ShelfItemView: View { let item: ShelfItem @EnvironmentObject var vm: BoringViewModel @ObservedObject var selection = ShelfSelectionModel.shared @StateObject private var viewModel: ShelfItemViewModel @EnvironmentObject private var quickLookService: QuickLookService @State private var showStack = false @State private var cachedPreviewImage: NSImage? @State private var debouncedDropTarget = false private var isSelected: Bool { viewModel.isSelected } private var shouldHideDuringDrag: Bool { selection.isDragging && selection.isSelected(item.id) && false } init(item: ShelfItem) { self.item = item _viewModel = StateObject(wrappedValue: ShelfItemViewModel(item: item)) } var body: some View { ZStack { if !shouldHideDuringDrag { VStack(alignment: .center, spacing: 2) { iconView textView } .frame(width: 105) .padding(.vertical, 10) .padding(.horizontal, 5) .background(backgroundView) .contentShape(Rectangle()) .animation(.easeInOut(duration: 0.1), value: debouncedDropTarget) .animation(.easeInOut(duration: 0.1), value: isSelected) DraggableClickHandler( item: item, viewModel: viewModel, cachedPreviewImage: $cachedPreviewImage, dragPreviewContent: { DragPreviewView(thumbnail: viewModel.thumbnail ?? item.icon, displayName: item.displayName) }, onRightClick: viewModel.handleRightClick, onClick: { event, nsview in viewModel.handleClick(event: event, view: nsview) } ) } else { Color.clear .frame(width: 105) .padding(.vertical, 10) .padding(.horizontal, 5) } } .onChange(of: viewModel.isDropTargeted) { _, targeted in vm.dragDetectorTargeting = targeted // Debounce drop target state changes Task { @MainActor in try? await Task.sleep(for: .milliseconds(50)) debouncedDropTarget = targeted } } .onAppear { Task { await viewModel.loadThumbnail() // Pre-render drag preview once on appear if cachedPreviewImage == nil { cachedPreviewImage = await renderDragPreview() } } viewModel.onQuickLookRequest = { urls in quickLookService.show(urls: urls, selectFirst: true) } } .onChange(of: viewModel.thumbnail) { _, _ in // Invalidate cached preview when thumbnail changes Task { cachedPreviewImage = await renderDragPreview() } } .quickLookPresenter(using: quickLookService) } // MARK: - View Components private var iconView: some View { Image(nsImage: viewModel.thumbnail ?? item.icon) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 56, height: 56) .clipShape(RoundedRectangle(cornerRadius: 12)) .shadow(color: .black.opacity(0.15), radius: 3, x: 0, y: 2) } private var textView: some View { Text(item.displayName) .font(.system(size: 12, weight: .medium)) .foregroundStyle(.primary) .lineLimit(2) .truncationMode(.middle) .multilineTextAlignment(.center) .frame(height: 30, alignment: .top) } private var backgroundView: some View { RoundedRectangle(cornerRadius: 12, style: .continuous) .fill(backgroundColor) .overlay( RoundedRectangle(cornerRadius: 12, style: .continuous) .strokeBorder( strokeColor, lineWidth: strokeWidth ) ) } private var backgroundColor: Color { if debouncedDropTarget { return Color.accentColor.opacity(0.25) } else if isSelected { return Color.accentColor.opacity(0.15) } else { return Color.clear } } private var strokeColor: Color { if debouncedDropTarget { return Color.accentColor.opacity(0.9) } else if isSelected { return Color.accentColor.opacity(0.8) } else { return Color.clear } } private var strokeWidth: CGFloat { if debouncedDropTarget { return 3 } else if isSelected { return 2 } else { return 1 } } // MARK: - Drag Preview Rendering @MainActor private func renderDragPreview() async -> NSImage { let content = DragPreviewView(thumbnail: viewModel.thumbnail ?? item.icon, displayName: item.displayName) let renderer = ImageRenderer(content: content) renderer.scale = NSScreen.main?.backingScaleFactor ?? 2.0 return renderer.nsImage ?? (viewModel.thumbnail ?? item.icon) } } // MARK: - Draggable Click Handler with NSDraggingSource private struct DraggableClickHandler: NSViewRepresentable { let item: ShelfItem let viewModel: ShelfItemViewModel @Binding var cachedPreviewImage: NSImage? @ViewBuilder let dragPreviewContent: () -> Content let onRightClick: (NSEvent, NSView) -> Void let onClick: (NSEvent, NSView) -> Void func makeNSView(context: Context) -> DraggableClickView { let view = DraggableClickView() view.item = item view.viewModel = viewModel view.dragPreviewImage = cachedPreviewImage ?? renderDragPreview() view.onRightClick = onRightClick view.onClick = onClick return view } func updateNSView(_ nsView: DraggableClickView, context: Context) { nsView.item = item nsView.viewModel = viewModel // Only update preview if cached version is available if let cached = cachedPreviewImage { nsView.dragPreviewImage = cached } nsView.onRightClick = onRightClick nsView.onClick = onClick } private func renderDragPreview() -> NSImage { let content = dragPreviewContent() let renderer = ImageRenderer(content: content) renderer.scale = NSScreen.main?.backingScaleFactor ?? 2.0 if let nsImage = renderer.nsImage { return nsImage } // Fallback to icon if rendering fails return viewModel.thumbnail ?? item.icon } final class DraggableClickView: NSView, NSDraggingSource { var item: ShelfItem! weak var viewModel: ShelfItemViewModel? var dragPreviewImage: NSImage? var onRightClick: ((NSEvent, NSView) -> Void)? var onClick: ((NSEvent, NSView) -> Void)? private var mouseDownEvent: NSEvent? private let dragThreshold: CGFloat = 3.0 private var draggedURLs: [URL] = [] private var draggedItems: [ShelfItem] = [] override func rightMouseDown(with event: NSEvent) { onRightClick?(event, self) } override func mouseDown(with event: NSEvent) { mouseDownEvent = event onClick?(event, self) } override func mouseDragged(with event: NSEvent) { guard let mouseDownEvent = mouseDownEvent else { super.mouseDragged(with: event) return } let dragDistance = hypot( event.locationInWindow.x - mouseDownEvent.locationInWindow.x, event.locationInWindow.y - mouseDownEvent.locationInWindow.y ) if dragDistance > dragThreshold { startDragSession(with: event) self.mouseDownEvent = nil } else { super.mouseDragged(with: event) } } private func startDragSession(with event: NSEvent) { // Prepare dragging items let selectedItems = ShelfSelectionModel.shared.selectedItems(in: ShelfStateViewModel.shared.items) let itemsToDrag: [ShelfItem] if selectedItems.count > 1 && selectedItems.contains(where: { $0.id == item.id }) { itemsToDrag = selectedItems } else { itemsToDrag = [item] } // Store items being dragged for auto-remove feature draggedItems = itemsToDrag // Create dragging items for AppKit var draggingItems: [NSDraggingItem] = [] for dragItem in itemsToDrag { if let pasteboardItem = createPasteboardItem(for: dragItem) { let draggingItem = NSDraggingItem(pasteboardWriter: pasteboardItem) // Use the drag preview image let image = dragPreviewImage ?? dragItem.icon let imageFrame = NSRect( x: 0, y: 0, width: image.size.width, height: image.size.height ) draggingItem.setDraggingFrame(imageFrame, contents: image) draggingItems.append(draggingItem) } } guard !draggingItems.isEmpty else { return } beginDraggingSession(with: draggingItems, event: event, source: self) } private func createPasteboardItem(for item: ShelfItem) -> NSPasteboardItem? { let pasteboardItem = NSPasteboardItem() switch item.kind { case .file: guard let url = ShelfStateViewModel.shared.resolveAndUpdateBookmark(for: item) else { pasteboardItem.setString(item.displayName, forType: .string) return pasteboardItem } // Start accessing security-scoped resource and keep it active during drag if url.startAccessingSecurityScopedResource() { draggedURLs.append(url) NSLog("🔐 Started security-scoped access for drag: \(url.path)") } pasteboardItem.setString(url.absoluteString, forType: .fileURL) pasteboardItem.setString(url.path, forType: .string) return pasteboardItem case .text(let string): pasteboardItem.setString(string, forType: .string) return pasteboardItem case .link(let url): pasteboardItem.setString(url.absoluteString, forType: .URL) pasteboardItem.setString(url.absoluteString, forType: .string) return pasteboardItem } } // MARK: - NSDraggingSource func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation { // When copyOnDrag is enabled, only allow copy operations if Defaults[.copyOnDrag] { return [.copy] } switch context { case .outsideApplication: return [.copy, .move] case .withinApplication: return [.copy, .move, .generic] @unknown default: return [.copy] } } func draggingSession(_ session: NSDraggingSession, willBeginAt screenPoint: NSPoint) { ShelfSelectionModel.shared.beginDrag() } func draggingSession(_ session: NSDraggingSession, endedAt screenPoint: NSPoint, operation: NSDragOperation) { ShelfSelectionModel.shared.endDrag() // Stop accessing security-scoped resources after drag completes for url in draggedURLs { url.stopAccessingSecurityScopedResource() NSLog("🔐 Stopped security-scoped access after drag: \(url.path)") } draggedURLs.removeAll() // Auto-remove items from shelf if enabled and drag succeeded if Defaults[.autoRemoveShelfItems] && !operation.isEmpty { for item in draggedItems { ShelfStateViewModel.shared.remove(item) } } draggedItems.removeAll() } func ignoreModifierKeys(for session: NSDraggingSession) -> Bool { return false } } } ================================================ FILE: boringNotch/components/Shelf/Views/ShelfView.swift ================================================ // // ShelfItemView.swift // boringNotch // // Created by Alexander on 2025-09-24. // import SwiftUI import AppKit struct ShelfView: View { @EnvironmentObject var vm: BoringViewModel @StateObject var tvm = ShelfStateViewModel.shared @StateObject var selection = ShelfSelectionModel.shared @StateObject private var quickLookService = QuickLookService() private let spacing: CGFloat = 8 var body: some View { HStack(spacing: 12) { FileShareView() .aspectRatio(1, contentMode: .fit) .environmentObject(vm) panel .onDrop(of: [.fileURL, .url, .utf8PlainText, .plainText, .data], isTargeted: $vm.dragDetectorTargeting) { providers in handleDrop(providers: providers) } } // Bind Quick Look to shelf selection .onChange(of: selection.selectedIDs) { updateQuickLookSelection() } .quickLookPresenter(using: quickLookService) } private func handleDrop(providers: [NSItemProvider]) -> Bool { guard !selection.isDragging else { return false } vm.dropEvent = true ShelfStateViewModel.shared.load(providers) return true } private func updateQuickLookSelection() { guard quickLookService.isQuickLookOpen && !selection.selectedIDs.isEmpty else { return } let selectedItems = selection.selectedItems(in: tvm.items) let urls: [URL] = selectedItems.compactMap { item in if let fileURL = item.fileURL { return fileURL } if case .link(let url) = item.kind { return url } return nil } if !urls.isEmpty { quickLookService.updateSelection(urls: urls) } } var panel: some View { RoundedRectangle(cornerRadius: 16) .stroke( vm.dragDetectorTargeting ? Color.accentColor.opacity(0.9) : Color.white.opacity(0.1), style: StrokeStyle(lineWidth: 3, lineCap: .round, dash: [10]) ) .overlay { content .padding() } .transaction { transaction in transaction.animation = vm.animation } .contentShape(Rectangle()) .onTapGesture { selection.clear() } } var content: some View { Group { if tvm.isEmpty { VStack(spacing: 10) { Image(systemName: "tray.and.arrow.down") .symbolVariant(.fill) .symbolRenderingMode(.hierarchical) .foregroundStyle(.white, .gray) .imageScale(.large) Text("Drop files here") .foregroundStyle(.gray) .font(.system(.title3, design: .rounded)) .fontWeight(.medium) } } else { ScrollView(.horizontal) { HStack(spacing: spacing) { ForEach(tvm.items) { item in ShelfItemView(item: item) .environmentObject(quickLookService) } } } .padding(-spacing) .scrollIndicators(.never) .onDrop(of: [.fileURL, .url, .utf8PlainText, .plainText, .data], isTargeted: $vm.dragDetectorTargeting) { providers in handleDrop(providers: providers) } } } .onAppear { ShelfStateViewModel.shared.cleanupInvalidItems() } } } ================================================ FILE: boringNotch/components/Tabs/TabButton.swift ================================================ // // TabButton.swift // boringNotch // // Created by Hugo Persson on 2024-08-24. // import SwiftUI struct TabButton: View { let label: String let icon: String let selected: Bool let onClick: () -> Void var body: some View { Button(action: onClick) { Image(systemName: icon) .padding(.horizontal, 15) .contentShape(Capsule()) } .buttonStyle(PlainButtonStyle()) } } #Preview { TabButton(label: "Home", icon: "tray.fill", selected: true) { print("Tapped") } } ================================================ FILE: boringNotch/components/Tabs/TabSelectionView.swift ================================================ // // TabSelectionView.swift // boringNotch // // Created by Hugo Persson on 2024-08-25. // import SwiftUI struct TabModel: Identifiable { let id = UUID() let label: String let icon: String let view: NotchViews } let tabs = [ TabModel(label: "Home", icon: "house.fill", view: .home), TabModel(label: "Shelf", icon: "tray.fill", view: .shelf) ] struct TabSelectionView: View { @ObservedObject var coordinator = BoringViewCoordinator.shared @Namespace var animation var body: some View { HStack(spacing: 0) { ForEach(tabs) { tab in TabButton(label: tab.label, icon: tab.icon, selected: coordinator.currentView == tab.view) { withAnimation(.smooth) { coordinator.currentView = tab.view } } .frame(height: 26) .foregroundStyle(tab.view == coordinator.currentView ? .white : .gray) .background { if tab.view == coordinator.currentView { Capsule() .fill(coordinator.currentView == tab.view ? Color(nsColor: .secondarySystemFill) : Color.clear) .matchedGeometryEffect(id: "capsule", in: animation) } else { Capsule() .fill(coordinator.currentView == tab.view ? Color(nsColor: .secondarySystemFill) : Color.clear) .matchedGeometryEffect(id: "capsule", in: animation) .hidden() } } } } .clipShape(Capsule()) } } #Preview { BoringHeader().environmentObject(BoringViewModel()) } ================================================ FILE: boringNotch/components/TestView.swift ================================================ // // TestView.swift // boringNotch // // Created by Richard Kunkli on 14/08/2024. // import SwiftUI struct FluidSlider: View { private let color: Color = Color.white @State private var offset: CGFloat = 0 var rectSize = CGSize(width: 300, height: 50) var rectSize2 = CGSize(width: 200, height: 18) var circleSize: CGFloat = 35 @GestureState var isDragging: Bool = false @State var previousOffset: CGFloat = 0 @State private var isBeating: Bool = false var body: some View { HStack { slider .frame(width: rectSize2.width, height: circleSize) } .padding() .background(.black) } private var slider: some View { ZStack { Canvas { context, size in context.addFilter(.alphaThreshold(min: 0.5, max: 1, color: color)) context.addFilter(.blur(radius: 10)) context.drawLayer { ctx in if let rectangle = ctx.resolveSymbol(id: "Capsule") { ctx.draw(rectangle, at: CGPoint(x: size.width/2, y: size.height/2)) } if let circle = ctx.resolveSymbol(id: "Circle") { ctx.draw(circle, at: CGPoint(x: size.width/2 - rectSize2.width/2 + circleSize/2, y: size.height/2)) } } } symbols: { Capsule() .frame(width: rectSize2.width, height: rectSize2.height, alignment: .center) .tag("Capsule") Circle() .frame(width: circleSize, height: circleSize, alignment: .center) .offset(x: offset) .animation(.spring(), value: isDragging) .tag("Circle") } .simultaneousGesture( DragGesture(minimumDistance: 0) .updating($isDragging, body: { _, state, _ in state = true }) .onChanged({ value in self.offset = min(max(self.previousOffset + value.translation.width, 0), rectSize2.width - circleSize) }) .onEnded({ value in self.previousOffset = self.offset }) ) Circle() .fill(Color.black) .frame(width: circleSize * 0.6) .overlay { Image(systemName: "speaker.wave.2.fill") .imageScale(.small) } .offset(x: (-rectSize2.width/2) + (circleSize/2)) .offset(x: offset) .animation(.spring(), value: isDragging) .allowsHitTesting(false) } } private var animation: Animation { .spring(response: 0.5, dampingFraction: 0.6, blendDuration: 0.5) } private var percentage: Int { Int((offset) / (rectSize.width - circleSize) * 100) } } #Preview { FluidSlider() } ================================================ FILE: boringNotch/components/Tips/TipStore.swift ================================================ // // TipStore.swift // boringNotch // // Created by Richard Kunkli on 15/09/2024. // import SwiftUI import TipKit struct HUDsTip: Tip { var title: Text { Text("Enhance your experience with HUDs") } var message: Text? { Text("Unlock advanced features and improve your experience. Upgrade now for more customizations!") } var image: Image? { AppIcon(for: "theboringteam.boringNotch") } var actions: [Action] { Action { Text("More") } } } struct CBTip: Tip { var title: Text { Text("Boost your productivity with Clipboard Manager") } var message: Text? { Text("Easily copy, store, and manage your most-used content. Upgrade now for advanced features like multi-item storage and quick access!") } var image: Image? { AppIcon(for: "theboringteam.boringNotch") } var actions: [Action] { Action { Text("More") } } } struct TipsView: View { var hudTip = HUDsTip() var cbTip = CBTip() var body: some View { VStack { TipView(hudTip) TipView(cbTip) } .task { try? Tips.configure([ .displayFrequency(.immediate), .datastoreLocation(.applicationDefault) ]) } } } #Preview { TipsView() } ================================================ FILE: boringNotch/components/Webcam/WebcamView.swift ================================================ // // WebcamView.swift // boringNotch // // Created by Harsh Vardhan Goswami on 19/08/24. // import AVFoundation import Defaults import SwiftUI struct CameraPreviewView: View { @EnvironmentObject var vm: BoringViewModel @ObservedObject var webcamManager: WebcamManager // Track if authorization request is in progress to avoid multiple requests @State private var isRequestingAuthorization: Bool = false var body: some View { GeometryReader { geometry in ZStack { if let previewLayer = webcamManager.previewLayer { CameraPreviewLayerView(previewLayer: previewLayer) .scaleEffect(x: -1, y: 1) .clipShape(RoundedRectangle(cornerRadius: Defaults[.mirrorShape] == .rectangle ? !Defaults[.cornerRadiusScaling] ? MusicPlayerImageSizes.cornerRadiusInset.closed : MusicPlayerImageSizes.cornerRadiusInset.opened : 100)) .frame(width: geometry.size.width, height: geometry.size.width) .opacity(webcamManager.isSessionRunning ? 1 : 0) } if !webcamManager.isSessionRunning { ZStack { RoundedRectangle(cornerRadius: Defaults[.mirrorShape] == .rectangle ? !Defaults[.cornerRadiusScaling] ? MusicPlayerImageSizes.cornerRadiusInset.closed : 12 : 100) .fill(Color(red: 20/255, green: 20/255, blue: 20/255)) .strokeBorder(.white.opacity(0.04), lineWidth: 1) .frame(width: geometry.size.width, height: geometry.size.width) VStack(spacing: 8) { Image(systemName: webcamManager.authorizationStatus == .denied ? "exclamationmark.triangle" : "web.camera") .foregroundStyle(.gray) .font(.system(size: geometry.size.width/3.5)) Text(webcamManager.authorizationStatus == .denied ? "Access Denied" : "Mirror") .font(.caption2) .foregroundColor(.gray) } } } } .onTapGesture { handleCameraTap() } .onDisappear { webcamManager.stopSession() } } .aspectRatio(1, contentMode: .fit) } private func handleCameraTap() { if isRequestingAuthorization { return // Prevent multiple authorization requests } switch webcamManager.authorizationStatus { case .authorized: if webcamManager.isSessionRunning { webcamManager.stopSession() } else if webcamManager.cameraAvailable { webcamManager.startSession() } case .denied, .restricted: DispatchQueue.main.async { let alert = NSAlert() alert.messageText = "Camera Access Required" alert.informativeText = "Please allow camera access in System Settings to use the mirror feature." alert.addButton(withTitle: "Open System Settings") alert.addButton(withTitle: "Cancel") if alert.runModal() == .alertFirstButtonReturn { if let settingsURL = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Camera") { NSWorkspace.shared.open(settingsURL) } } } case .notDetermined: isRequestingAuthorization = true webcamManager.checkAndRequestVideoAuthorization() // Reset the request flag after a reasonable delay DispatchQueue.main.asyncAfter(deadline: .now() + 2) { isRequestingAuthorization = false } @unknown default: break } } } struct CameraPreviewLayerView: NSViewRepresentable { let previewLayer: AVCaptureVideoPreviewLayer func makeNSView(context: Context) -> NSView { let view = NSView(frame: .zero) previewLayer.frame = view.bounds previewLayer.videoGravity = .resizeAspectFill view.layer = previewLayer view.wantsLayer = true return view } func updateNSView(_ nsView: NSView, context: Context) { CATransaction.begin() CATransaction.setDisableActions(true) previewLayer.frame = nsView.bounds CATransaction.commit() } } #Preview { CameraPreviewView(webcamManager: .shared) } ================================================ FILE: boringNotch/components/WhatsNewView.swift ================================================ // // WhatsNewView.swift // boringNotch // // Created by Richard Kunkli on 09/08/2024. // import SwiftUI struct WhatsNewView: View { @Binding var isPresented: Bool var body: some View { VStack(spacing: 20) { Text("What's New") .font(.largeTitle) VStack(alignment: .leading, spacing: 10) { Text("• New feature 1") Text("• Improved performance") Text("• Bug fixes") } Button("Got it!") { isPresented = false } } .frame(width: 300, height: 200) .padding() } } #Preview { WhatsNewView(isPresented: .constant(true)) } ================================================ FILE: boringNotch/enums/generic.swift ================================================ // // generic.swift // boringNotch // // Created by Harsh Vardhan Goswami on 04/08/24. // import Foundation import Defaults public enum Style { case notch case floating } public enum ContentType: Int, Codable, Hashable, Equatable { case normal case menu case settings } public enum NotchState { case closed case open } public enum NotchViews { case home case shelf } enum SettingsEnum { case general case about case charge case download case mediaPlayback case hud case shelf case extensions } enum DownloadIndicatorStyle: String, Defaults.Serializable { case progress = "Progress" case percentage = "Percentage" } enum DownloadIconStyle: String, Defaults.Serializable { case onlyAppIcon = "Only app icon" case onlyIcon = "Only download icon" case iconAndAppIcon = "Icon and app icon" } enum MirrorShapeEnum: String, Defaults.Serializable { case rectangle = "Rectangular" case circle = "Circular" } enum WindowHeightMode: String, Defaults.Serializable { case matchMenuBar = "Match menubar height" case matchRealNotchSize = "Match real notch height" case custom = "Custom height" } enum SliderColorEnum: String, CaseIterable, Defaults.Serializable { case white = "White" case albumArt = "Match album art" case accent = "Accent color" } ================================================ FILE: boringNotch/extensions/ActionBar.swift ================================================ // // ActionBar.swift // boringNotch // // Created by Richard Kunkli on 15/09/2024. // import SwiftUI extension View { func actionBar(padding: CGFloat = 10, @ViewBuilder content: () -> Content) -> some View { self .padding(.bottom, 24) .overlay(alignment: .bottom) { VStack(spacing: -1) { Divider() HStack(spacing: 0) { content() .buttonStyle(PlainButtonStyle()) } .frame(height: 16) .padding(.vertical, 4) .padding(.horizontal, padding) .frame(maxWidth: .infinity, alignment: .leading) } .frame(height: 24) .background(.separator) } } } ================================================ FILE: boringNotch/extensions/BundleInfos.swift ================================================ // // BundleInfos.swift // boringNotch // // Created by Richard Kunkli on 08/08/2024. // import SwiftUI extension Bundle { var releaseVersionNumber: String? { return infoDictionary?["CFBundleShortVersionString"] as? String } var buildVersionNumber: String? { return infoDictionary?["CFBundleVersion"] as? String } var releaseVersionNumberPretty: String { return "v\(releaseVersionNumber ?? "1.0.0")" } var iconFileName: String? { guard let icons = infoDictionary?["CFBundleIcons"] as? [String: Any], let primaryIcon = icons["CFBundlePrimaryIcon"] as? [String: Any], let iconFiles = primaryIcon["CFBundleIconFiles"] as? [String], let iconFileName = iconFiles.last else { return nil } return iconFileName } } struct BundleAppIcon: View { var body: some View { Bundle.main.iconFileName .flatMap { NSImage(named: $0) } .map { Image(nsImage: $0) } } } func isNewVersion() -> Bool { let defaults = UserDefaults.standard let currentVersion = Bundle.main.releaseVersionNumber ?? "1.0" let savedVersion = defaults.string(forKey: "LastVersionRun") ?? "" if currentVersion != savedVersion { defaults.set(currentVersion, forKey: "LastVersionRun") return true } return false } func isExtensionRunning(_ bundleID: String) -> Bool { if let _ = NSWorkspace.shared.runningApplications.first(where: {$0.bundleIdentifier == bundleID}) { return true } return false } ================================================ FILE: boringNotch/extensions/Button+Bouncing.swift ================================================ // // Button+Bouncing.swift // boringNotch // // Created by Harsh Vardhan Goswami on 19/08/24. // import SwiftUI import Defaults struct BouncingButtonStyle: ButtonStyle { let vm: BoringViewModel @State private var isPressed = false func makeBody(configuration: Configuration) -> some View { configuration.label .padding(12) .background( RoundedRectangle(cornerRadius: Defaults[.cornerRadiusScaling] ? 10 : MusicPlayerImageSizes.cornerRadiusInset.closed) .fill(Color(red: 20/255, green: 20/255, blue: 20/255)) .strokeBorder(.white.opacity(0.04), lineWidth: 1) ) .scaleEffect(isPressed ? 0.9 : 1.0) .onChange(of: configuration.isPressed) { _, _ in withAnimation(.spring(response: 0.3, dampingFraction: 0.3, blendDuration: 0.3)) { isPressed.toggle() } } } } extension Button { func bouncingStyle(vm: BoringViewModel) -> some View { self.buttonStyle(BouncingButtonStyle(vm: vm)) } } ================================================ FILE: boringNotch/extensions/Color+AccentColor.swift ================================================ // // Color+AccentColor.swift // boringNotch // // Created by Alexander on 2025-10-24. // import SwiftUI import Defaults extension Color { static var effectiveAccent: Color { if Defaults[.useCustomAccentColor], let colorData = Defaults[.customAccentColorData], let nsColor = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSColor.self, from: colorData) { return Color(nsColor: nsColor) } return .accentColor } /// Returns a darker version of the accent color suitable for backgrounds static var effectiveAccentBackground: Color { if Defaults[.useCustomAccentColor], let colorData = Defaults[.customAccentColorData], let nsColor = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSColor.self, from: colorData) { return Color(nsColor: nsColor.withSystemEffect(.disabled)) } return Color.effectiveAccent.opacity(0.25) } } extension NSColor { static var effectiveAccent: NSColor { if Defaults[.useCustomAccentColor], let colorData = Defaults[.customAccentColorData], let nsColor = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSColor.self, from: colorData) { return nsColor } return NSColor.controlAccentColor } /// Returns a darker version of the accent color as NSColor suitable for backgrounds static var effectiveAccentBackground: NSColor { if Defaults[.useCustomAccentColor], let colorData = Defaults[.customAccentColorData], let nsColor = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSColor.self, from: colorData) { return nsColor.withSystemEffect(.disabled) } return NSColor.controlAccentColor.withAlphaComponent(0.25) } } ================================================ FILE: boringNotch/extensions/ConditionalModifier.swift ================================================ // // ConditionalModifier.swift // boringNotch // // Created by Richard Kunkli on 20/08/2024. // import SwiftUI extension View { @ViewBuilder func conditionalModifier(_ condition: Bool, transform: (Self) -> Content) -> some View { if condition { transform(self) } else { self } } } ================================================ FILE: boringNotch/extensions/DataTypes+Extensions.swift ================================================ // // DataTypes+Extensions.swift // boringNotch // // Created by Harsh Vardhan Goswami on 27/08/24. // import Foundation extension Date { static var yesterday: Date { return Date().dayBefore } static var tomorrow: Date { return Date().dayAfter } var dayBefore: Date { return Calendar.current.date(byAdding: .day, value: -1, to: noon)! } var dayAfter: Date { return Calendar.current.date(byAdding: .day, value: 1, to: noon)! } var noon: Date { return Calendar.current.date(bySettingHour: 12, minute: 0, second: 0, of: self)! } var date: String { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "dd" return dateFormatter.string(from: self) } var month: String { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "MMMM" return dateFormatter.string(from: self) } func dayOfTheWeek(dayOfWeek: Int) -> String { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "EEE" let date = Calendar.current.date(bySetting: .weekday, value: dayOfWeek, of: self) ?? self return dateFormatter.string(from: date) } } extension NSSize { var s: String { "\(width.i)×\(height.i)" } var aspectRatio: Double { width / height } func scaled(by factor: Double) -> CGSize { CGSize(width: (width * factor).evenInt, height: (height * factor).evenInt) } } extension Int { var s: String { String(self) } var d: Double { Double(self) } } extension Double { @inline(__always) @inlinable var intround: Int { rounded().i } @inline(__always) @inlinable var i: Int { Int(self) } var evenInt: Int { let x = intround return x + x % 2 } } extension CGFloat { @inline(__always) @inlinable var intround: Int { rounded().i } @inline(__always) @inlinable var i: Int { Int(self) } var evenInt: Int { let x = intround return x + x % 2 } } ================================================ FILE: boringNotch/extensions/KeyboardShortcutsHelper.swift ================================================ // // KeyboardShortcutsHelper.swift // boringNotch // // Created by Richard Kunkli on 16/08/2024. // import KeyboardShortcuts import SwiftUI import Carbon extension View { public func keyboardShortcut(_ shortcut: KeyboardShortcuts.Name) -> some View { if let shortcut = shortcut.shortcut { if let keyEquivalent = shortcut.toKeyEquivalent() { return AnyView(self.keyboardShortcut(keyEquivalent, modifiers: shortcut.toEventModifiers())) } } return AnyView(self) } } extension KeyboardShortcuts.Shortcut { func toKeyEquivalent() -> KeyEquivalent? { let carbonKeyCode = UInt16(self.carbonKeyCode) let maxNameLength = 4 var nameBuffer = [UniChar](repeating: 0, count : maxNameLength) var nameLength = 0 let modifierKeys = UInt32(alphaLock >> 8) & 0xFF // Caps Lock var deadKeys: UInt32 = 0 let keyboardType = UInt32(LMGetKbdType()) let source = TISCopyCurrentKeyboardLayoutInputSource().takeRetainedValue() guard let ptr = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData) else { NSLog("Could not get keyboard layout data") return nil } let layoutData = Unmanaged.fromOpaque(ptr).takeUnretainedValue() as Data let osStatus = layoutData.withUnsafeBytes { UCKeyTranslate($0.bindMemory(to: UCKeyboardLayout.self).baseAddress, carbonKeyCode, UInt16(kUCKeyActionDown), modifierKeys, keyboardType, UInt32(kUCKeyTranslateNoDeadKeysMask), &deadKeys, maxNameLength, &nameLength, &nameBuffer) } guard osStatus == noErr else { NSLog("Code: 0x%04X Status: %+i", carbonKeyCode, osStatus); return nil } return KeyEquivalent(Character(String(utf16CodeUnits: nameBuffer, count: nameLength))) } func toEventModifiers() -> SwiftUI.EventModifiers { var modifiers: SwiftUI.EventModifiers = [] if self.modifiers.contains(NSEvent.ModifierFlags.command) { modifiers.update(with: EventModifiers.command) } if self.modifiers.contains(NSEvent.ModifierFlags.control) { modifiers.update(with: EventModifiers.control) } if self.modifiers.contains(NSEvent.ModifierFlags.option) { modifiers.update(with: EventModifiers.option) } if self.modifiers.contains(NSEvent.ModifierFlags.shift) { modifiers.update(with: EventModifiers.shift) } if self.modifiers.contains(NSEvent.ModifierFlags.capsLock) { modifiers.update(with: EventModifiers.capsLock) } if self.modifiers.contains(NSEvent.ModifierFlags.numericPad) { modifiers.update(with: EventModifiers.numericPad) } return modifiers } } ================================================ FILE: boringNotch/extensions/MouseTracker.swift ================================================ // // MouseTracker.swift // boringNotch // // Created by Richard Kunkli on 12/08/2024. // import SwiftUI extension NSScreen { static var screenWithMouse: NSScreen? { let mouseLocation = NSEvent.mouseLocation let screens = NSScreen.screens let screenWithMouse = (screens.first { NSMouseInRect(mouseLocation, $0.frame, false) }) return screenWithMouse } } ================================================ FILE: boringNotch/extensions/NSImage+Extensions.swift ================================================ // // Image2Color.swift // boringNotch // // Created by Richard Kunkli on 07/08/2024. // import SwiftUI import AppKit import Cocoa import Foundation import CoreImage import CoreGraphics import CoreImage.CIFilterBuiltins extension NSImage { func averageColor(completion: @escaping (NSColor?) -> Void) { DispatchQueue.global(qos: .userInitiated).async { guard let cgImage = self.cgImage(forProposedRect: nil, context: nil, hints: nil) else { DispatchQueue.main.async { completion(nil) } return } let width = cgImage.width let height = cgImage.height let totalPixels = width * height guard let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width * 4, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else { DispatchQueue.main.async { completion(nil) } return } context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height)) guard let data = context.data else { DispatchQueue.main.async { completion(nil) } return } let pointer = data.bindMemory(to: UInt32.self, capacity: totalPixels) var totalRed: UInt64 = 0 var totalGreen: UInt64 = 0 var totalBlue: UInt64 = 0 for i in 0..> 8) & 0xFF) totalBlue += UInt64((color >> 16) & 0xFF) } let averageRed = CGFloat(totalRed) / CGFloat(totalPixels) / 255.0 let averageGreen = CGFloat(totalGreen) / CGFloat(totalPixels) / 255.0 let averageBlue = CGFloat(totalBlue) / CGFloat(totalPixels) / 255.0 let minBrightness: CGFloat = 0.5 let isNearBlack = averageRed < 0.03 && averageGreen < 0.03 && averageBlue < 0.03 var finalColor: NSColor if isNearBlack { // If it's near black, just return a gray color with the minimum brightness finalColor = NSColor(white: minBrightness, alpha: 1.0) } else { var color = NSColor(red: averageRed, green: averageGreen, blue: averageBlue, alpha: 1.0) var hue: CGFloat = 0 var saturation: CGFloat = 0 var brightness: CGFloat = 0 var alpha: CGFloat = 0 color.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) if brightness < minBrightness { // Increase brightness while maintaining hue and reducing saturation let saturationScale = brightness / minBrightness color = NSColor(hue: hue, saturation: saturation * saturationScale, brightness: minBrightness, alpha: alpha) } finalColor = color } DispatchQueue.main.async { completion(finalColor) } } } func getBrightness() -> CGFloat { guard let cgImage = self.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return 0 } let inputImage = CIImage(cgImage: cgImage) let filter = CIFilter.areaAverage() filter.inputImage = inputImage filter.extent = inputImage.extent guard let outputImage = filter.outputImage else { return 0 } let context = CIContext(options: nil) var bitmap = [UInt8](repeating: 0, count: 4) context.render(outputImage, toBitmap: &bitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: .RGBA8, colorSpace: CGColorSpaceCreateDeviceRGB()) let brightness = (0.2126 * CGFloat(bitmap[0]) + 0.7152 * CGFloat(bitmap[1]) + 0.0722 * CGFloat(bitmap[2])) / 255.0 return brightness } } extension Color { func ensureMinimumBrightness(factor: CGFloat) -> Color { guard factor >= 0 && factor <= 1 else { return self // Return original color if factor is out of bounds } let nsColor = NSColor(self) // Convert to RGB color space guard let rgbColor = nsColor.usingColorSpace(.sRGB) else { return self // Return original color if conversion fails } var red: CGFloat = 0 var green: CGFloat = 0 var blue: CGFloat = 0 var alpha: CGFloat = 0 rgbColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha) // Calculate perceived brightness using the formula: (0.299*R + 0.587*G + 0.114*B) let perceivedBrightness = (0.2126 * red + 0.7152 * green + 0.0722 * blue) let scale = factor / perceivedBrightness red = min(red * scale, 1.0) green = min(green * scale, 1.0) blue = min(blue * scale, 1.0) return Color(red: Double(red), green: Double(green), blue: Double(blue), opacity: Double(alpha)) } } ================================================ FILE: boringNotch/extensions/NSItemProvider+LoadHelpers.swift ================================================ // // NSItemProvider+LoadHelpers.swift // boringNotch // // Created by Alexander on 2025-09-24. // import AppKit import Foundation import UniformTypeIdentifiers extension NSItemProvider { func extractItem() async -> URL? { return await loadFileURL(typeIdentifier: UTType.item.identifier) } /// Detects if this is a file dragged from the filesystem func extractFileURL() async -> URL? { if hasItemConformingToTypeIdentifier(UTType.fileURL.identifier) { return await loadFileURL(typeIdentifier: UTType.fileURL.identifier) } return nil } /// Loads raw data for the given type identifier func loadData() async -> Data? { NSLog(String(describing: self.registeredTypeIdentifiers)) guard hasItemConformingToTypeIdentifier(UTType.data.identifier) else { return nil } return await withCheckedContinuation { (cont: CheckedContinuation) in loadItem(forTypeIdentifier: UTType.data.identifier, options: nil) { item, error in if let error = error { print("Error loading data for type \(UTType.data.identifier): \(error.localizedDescription)") cont.resume(returning: nil) return } if let url = item as? URL, let data = try? Data(contentsOf: url) { if !url.absoluteString.contains("com.apple.SwiftUI.filePromises") { cont.resume(returning: nil) return } self.suggestedName = self.suggestedName ?? url.lastPathComponent let fileManager = FileManager.default let folderURL = url.deletingLastPathComponent() do { // Delete the file first try fileManager.removeItem(at: url) print("Deleted file: \(url.path)") // Check folder contents let contents = try fileManager.contentsOfDirectory(atPath: folderURL.path) if contents.isEmpty { try fileManager.removeItem(at: folderURL) print("Folder was empty, deleted folder: \(folderURL.path)") } else { print("Folder not deleted — it still contains \(contents.count) item(s).") } } catch { print("Error: \(error.localizedDescription)") } cont.resume(returning: data) } else if let data = item as? Data { cont.resume(returning: data) } else { cont.resume(returning: nil) } } } } /// Attempts to extract a URL (web link) from the provider func extractURL() async -> URL? { if self.hasItemConformingToTypeIdentifier(UTType.url.identifier) { if let url = await loadURL(typeIdentifier: UTType.url.identifier) { //Validate URL guard url.scheme != nil else { return nil } return url } } return nil } func extractText() async -> String? { let textTypes = [UTType.utf8PlainText.identifier, UTType.plainText.identifier] for typeIdentifier in textTypes where self.hasItemConformingToTypeIdentifier(typeIdentifier) { if let text = await loadText(typeIdentifier: typeIdentifier) { return text } } return nil } /// Loads a file URL from the provider for the given type identifier. func loadFileURL(typeIdentifier: String) async -> URL? { await withCheckedContinuation { (cont: CheckedContinuation) in self.loadItem(forTypeIdentifier: typeIdentifier, options: nil) { item, error in if let error = error { print("❌ Error loading item for type \(typeIdentifier): \(error.localizedDescription)") cont.resume(returning: nil) return } var resolvedURL: URL? if let url = item as? URL { // Direct URL provided resolvedURL = url } else if let data = item as? Data { // Some providers hand out a UTF-8 file URL string, others a bookmark. Prefer parsing string first. if let string = String(data: data, encoding: .utf8) { if let url = URL(string: string) { resolvedURL = url } else if string.hasPrefix("/") { // Plain file system path resolvedURL = URL(fileURLWithPath: string) } } if resolvedURL == nil { // Fallback: try treating the data as a bookmark let bookmark = Bookmark(data: data) resolvedURL = bookmark.resolveURL() } } else if let string = item as? String { if let url = URL(string: string) { resolvedURL = url } else if string.hasPrefix("/") { resolvedURL = URL(fileURLWithPath: string) } } cont.resume(returning: resolvedURL) } } } /// Loads a URL from the provider for the given type identifier. func loadURL(typeIdentifier: String) async -> URL? { await withCheckedContinuation { (cont: CheckedContinuation) in self.loadItem(forTypeIdentifier: typeIdentifier, options: nil) { item, error in if error != nil { cont.resume(returning: nil) return } if let url = item as? URL { cont.resume(returning: url) } else if let data = item as? Data { if let string = String(data: data, encoding: .utf8) { if let url = URL(string: string) { cont.resume(returning: url) return } else if string.hasPrefix("/") { cont.resume(returning: URL(fileURLWithPath: string)) return } } cont.resume(returning: nil) } else if let string = item as? String { if let url = URL(string: string) { cont.resume(returning: url) } else if string.hasPrefix("/") { cont.resume(returning: URL(fileURLWithPath: string)) } else { cont.resume(returning: nil) } } else { cont.resume(returning: nil) } } } } /// Loads text from the provider for the given type identifier. func loadText(typeIdentifier: String) async -> String? { await withCheckedContinuation { (cont: CheckedContinuation) in self.loadItem(forTypeIdentifier: typeIdentifier, options: nil) { item, error in if error != nil { cont.resume(returning: nil) return } if let string = item as? String { cont.resume(returning: string) } else if let data = item as? Data, let string = String(data: data, encoding: .utf8) { cont.resume(returning: string) } else { cont.resume(returning: nil) } } } } } ================================================ FILE: boringNotch/extensions/NSMenu+AssociatedObject.swift ================================================ // // NSMenu+AssociatedObject.swift // boringNotch // // Created by Alexander on 2025-10-05. // import AppKit private final class MenuActionBox: NSObject { let target: AnyObject init(target: AnyObject) { self.target = target } } extension NSMenu { // Each NSMenu instance can store one retained target private static let retainedAction = AssociatedObject() func retainActionTarget(_ target: AnyObject) { NSMenu.retainedAction[self] = MenuActionBox(target: target) } } ================================================ FILE: boringNotch/extensions/NSScreen+UUID.swift ================================================ // // NSScreen+UUID.swift // boringNotch // // Created by Alexander on 2025-11-21. // import AppKit import CoreGraphics extension NSScreen { /// Returns a persistent UUID for this display var displayUUID: String? { guard let number = deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber else { return nil } let displayID = CGDirectDisplayID(number.uint32Value) guard let uuid = CGDisplayCreateUUIDFromDisplayID(displayID) else { return nil } let uuidString = CFUUIDCreateString(nil, uuid.takeRetainedValue()) as String return uuidString } /// Find a screen by its UUID @MainActor static func screen(withUUID uuid: String) -> NSScreen? { return NSScreenUUIDCache.shared.screen(forUUID: uuid) } /// Get UUID to NSScreen mapping for all screens @MainActor static var screensByUUID: [String: NSScreen] { return NSScreenUUIDCache.shared.allScreens } } /// Cache for UUID to NSScreen mappings to avoid repeated lookups @MainActor final class NSScreenUUIDCache { static let shared = NSScreenUUIDCache() private var cache: [String: NSScreen] = [:] private var observer: Any? private init() { rebuildCache() setupObserver() } deinit { if let observer = observer { NotificationCenter.default.removeObserver(observer) } } private func setupObserver() { observer = NotificationCenter.default.addObserver( forName: NSApplication.didChangeScreenParametersNotification, object: nil, queue: .main ) { [weak self] _ in self?.rebuildCache() } } private func rebuildCache() { var newCache: [String: NSScreen] = [:] for screen in NSScreen.screens { if let uuid = screen.displayUUID { newCache[uuid] = screen } } cache = newCache } func screen(forUUID uuid: String) -> NSScreen? { return cache[uuid] } var allScreens: [String: NSScreen] { return cache } } ================================================ FILE: boringNotch/extensions/PanGesture.swift ================================================ // // PanGesture.swift // boringNotch // // Created by Richard Kunkli on 21/08/2024. // import AppKit import SwiftUI enum PanDirection { case left, right, up, down var isHorizontal: Bool { self == .left || self == .right } var sign: CGFloat { (self == .right || self == .down) ? 1 : -1 } func signed(from translation: CGSize) -> CGFloat { (isHorizontal ? translation.width : translation.height) * sign } func signed(deltaX: CGFloat, deltaY: CGFloat) -> CGFloat { (isHorizontal ? deltaX : deltaY) * sign } } extension View { func panGesture(direction: PanDirection, threshold: CGFloat = 4, action: @escaping (CGFloat, NSEvent.Phase) -> Void) -> some View { self .gesture( DragGesture(minimumDistance: 0) .onChanged { value in let s = direction.signed(from: value.translation) guard s > 0, s.magnitude >= threshold else { return } action(s.magnitude, .changed) } .onEnded { _ in action(0, .ended) } ) .background(ScrollMonitor(direction: direction, threshold: threshold, action: action)) } } private struct ScrollMonitor: NSViewRepresentable { let direction: PanDirection let threshold: CGFloat let action: (CGFloat, NSEvent.Phase) -> Void func makeNSView(context: Context) -> NSView { let view = NSView() context.coordinator.installMonitor(on: view) return view } func updateNSView(_ nsView: NSView, context: Context) {} static func dismantleNSView(_ nsView: NSView, coordinator: Coordinator) { coordinator.removeMonitor() } func makeCoordinator() -> Coordinator { Coordinator(direction: direction, threshold: threshold, action: action) } @MainActor final class Coordinator: NSObject { private let direction: PanDirection private let threshold: CGFloat private let action: (CGFloat, NSEvent.Phase) -> Void private var monitor: Any? private var accumulated: CGFloat = 0 private var active = false private var endTask: Task? private let noiseThreshold: CGFloat = 0.2 init(direction: PanDirection, threshold: CGFloat, action: @escaping (CGFloat, NSEvent.Phase) -> Void) { self.direction = direction self.threshold = threshold self.action = action } private func scheduleEndTimeout() { // Cancel any existing scheduled end and schedule a new one. endTask?.cancel() endTask = Task { @MainActor in // If no new scroll event arrives within this window, consider the gesture ended. try? await Task.sleep(for: .milliseconds(300)) guard !Task.isCancelled else { return } if active { action(accumulated.magnitude, .ended) } else { action(0, .ended) } active = false accumulated = 0 } } func installMonitor(on view: NSView) { removeMonitor() monitor = NSEvent.addLocalMonitorForEvents(matching: [.scrollWheel]) { [weak self, weak view] event in guard let self = self, event.window === view?.window else { return event } self.handleScroll(event) return event } } func removeMonitor() { if let monitor = monitor { NSEvent.removeMonitor(monitor) self.monitor = nil } accumulated = 0 active = false endTask?.cancel() endTask = nil } private func handleScroll(_ event: NSEvent) { if event.phase == .ended || event.momentumPhase == .ended { if active { action(accumulated.magnitude, .ended) } else { action(0, .ended) } active = false accumulated = 0 return } // Only consider scroll events that are primarily along the configured axis. let absDX = abs(event.scrollingDeltaX) let absDY = abs(event.scrollingDeltaY) // Require the movement along the gesture axis to be at least 1.5x the orthogonal axis. let axisDominanceFactor: CGFloat = 1.5 let isAxisDominant: Bool = direction.isHorizontal ? (absDX >= axisDominanceFactor * absDY) : (absDY >= axisDominanceFactor * absDX) guard isAxisDominant else { return } // Scale non-precise (mouse wheel) scrolling deltas so they feel similar to // trackpad gestures. let raw = direction.signed(deltaX: event.scrollingDeltaX, deltaY: event.scrollingDeltaY) let scale: CGFloat = event.hasPreciseScrollingDeltas ? 1 : 8 let s = raw * scale guard s.magnitude > noiseThreshold else { return } accumulated = s > 0 ? accumulated + s : 0 if !active && accumulated >= threshold { active = true action(accumulated.magnitude, .began) } else if active { action(accumulated.magnitude, .changed) } // Schedule a timeout to end the gesture if no further scroll events arrive. scheduleEndTimeout() } } } ================================================ FILE: boringNotch/extensions/URL+SecurityScoped.swift ================================================ // // URL+SecurityScoped.swift // boringNotch // // Created by Alexander on 2025-10-07. // import Foundation import AppKit // MARK: - Error Types extension URL { func accessSecurityScopedResource(accessor: (URL) throws -> Value) rethrows -> Value { let didStartAccessing = startAccessingSecurityScopedResource() defer { if didStartAccessing { stopAccessingSecurityScopedResource() } } return try accessor(self) } /// Async version of accessSecurityScopedResource func accessSecurityScopedResource(accessor: (URL) async throws -> Value) async rethrows -> Value { let didStartAccessing = startAccessingSecurityScopedResource() defer { if didStartAccessing { stopAccessingSecurityScopedResource() } } return try await accessor(self) } } extension [URL] { func accessSecurityScopedResources(accessor: ([URL]) async throws -> Value) async rethrows -> Value { let didStart = self.map { $0.startAccessingSecurityScopedResource() } defer { for (url, started) in zip(self, didStart) where started { url.stopAccessingSecurityScopedResource() } } return try await accessor(self) } } ================================================ FILE: boringNotch/helpers/AppIcons.swift ================================================ // // AppIcons.swift // boringNotch // // Created by Harsh Vardhan Goswami on 16/08/24. // import SwiftUI import AppKit struct AppIcons { func getIcon(file path: String) -> NSImage? { guard FileManager.default.fileExists(atPath: path) else { return nil } return NSWorkspace.shared.icon(forFile: path) } func getIcon(bundleID: String) -> NSImage? { guard let path = NSWorkspace.shared.urlForApplication( withBundleIdentifier: bundleID )?.absoluteString else { return nil } return getIcon(file: path) } /// Easily read Info.plist as a Dictionary from any bundle by accessing .infoDictionary on Bundle func bundle(forBundleID: String) -> Bundle? { guard let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: forBundleID) else { return nil } return Bundle(url: url) } } func AppIcon(for bundleID: String) -> Image { let workspace = NSWorkspace.shared if let appURL = workspace.urlForApplication(withBundleIdentifier: bundleID) { let appIcon = workspace.icon(forFile: appURL.path) return Image(nsImage: appIcon) } return Image(nsImage: workspace.icon(for: .applicationBundle)) } func AppIconAsNSImage(for bundleID: String) -> NSImage? { let workspace = NSWorkspace.shared if let appURL = workspace.urlForApplication(withBundleIdentifier: bundleID) { let appIcon = workspace.icon(forFile: appURL.path) appIcon.size = NSSize(width: 256, height: 256) return appIcon } return nil } ================================================ FILE: boringNotch/helpers/AppleScriptHelper.swift ================================================ // // AppleScriptHelper.swift // boringNotch // // Created by Alexander on 2025-03-29. // import Foundation class AppleScriptHelper { @discardableResult class func execute(_ scriptText: String) async throws -> NSAppleEventDescriptor? { try await withCheckedThrowingContinuation { continuation in Task.detached(priority: .userInitiated) { let script = NSAppleScript(source: scriptText) var error: NSDictionary? if let descriptor = script?.executeAndReturnError(&error) { continuation.resume(returning: descriptor) } else if let error = error { continuation.resume(throwing: NSError(domain: "AppleScriptError", code: 1, userInfo: error as? [String: Any])) } else { continuation.resume(throwing: NSError(domain: "AppleScriptError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Unknown error"])) } } } } class func executeVoid(_ scriptText: String) async throws { _ = try await execute(scriptText) } } ================================================ FILE: boringNotch/helpers/ApplicationRelauncher.swift ================================================ // // ApplicationRelauncher.swift // boringNotch // // Created by Corentin132 on 03/10/2025. // import AppKit enum ApplicationRelauncher { static func restart() { guard let bundleIdentifier = Bundle.main.bundleIdentifier else { return } let workspace = NSWorkspace.shared guard let appURL = workspace.urlForApplication(withBundleIdentifier: bundleIdentifier) else { return } let configuration = NSWorkspace.OpenConfiguration() configuration.createsNewApplicationInstance = true workspace.openApplication(at: appURL, configuration: configuration, completionHandler: nil) NSApplication.shared.terminate(nil) } } ================================================ FILE: boringNotch/helpers/AssociatedObject.swift ================================================ // // AssociatedObject.swift // boringNotch // // Created by Alexander on 2025-10-05. // import Foundation import ObjectiveC /// Lightweight helper for Objective-C associated objects. public struct AssociatedObject { private let key: UnsafeRawPointer private let policy: objc_AssociationPolicy public init(_ policy: objc_AssociationPolicy = .OBJC_ASSOCIATION_RETAIN_NONATOMIC) { self.key = UnsafeRawPointer(Unmanaged.passUnretained(UniqueKey()).toOpaque()) self.policy = policy } private final class UniqueKey {} public subscript(_ owner: Owner) -> Value? { get { objc_getAssociatedObject(owner, key) as? Value } nonmutating set { objc_setAssociatedObject(owner, key, newValue, policy) } } } extension AssociatedObject: @unchecked Sendable where Value: Sendable {} ================================================ FILE: boringNotch/helpers/AudioPlayer.swift ================================================ // // AudioPlayer.swift // boringNotch // // Created by Harsh Vardhan Goswami on 09/08/24. // import Foundation import AppKit class AudioPlayer { func play(fileName: String, fileExtension: String) { NSSound(contentsOf:Bundle.main.url(forResource: fileName, withExtension: fileExtension)!, byReference: false)?.play() } } ================================================ FILE: boringNotch/helpers/Clipboard+Content.swift ================================================ import AppKit func getAttributedString(content: Any, type: NSPasteboard.PasteboardType) -> NSAttributedString? { if let stringContent = content as? String { return NSAttributedString(string: stringContent) } if type == .rtf, let data = content as? Data { return NSAttributedString(rtf: data, documentAttributes: nil) } else if type == .html, let data = content as? Data { return try? NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil) } else if type.rawValue == "public.utf8-plain-text", let data = content as? Data { return try? NSAttributedString(data: data, documentAttributes: nil) } else if type == .string { return NSAttributedString(string: content as? String ?? "") } else if type == .fileURL { return NSAttributedString(string: content as? String ?? "") } else if type == NSPasteboard.PasteboardType("com.apple.webarchive"), let data = content as? Data { return try? NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.webArchive], documentAttributes: nil) } return nil } func isText(type: NSPasteboard.PasteboardType) -> Bool { return type == .string || type == .html || type == .rtf || type == .html || type == .string || type.rawValue == "public.utf8-plain-text" || type.rawValue == "public.utf16-external-plain-text" || type.rawValue == "com.apple.webarchive" } ================================================ FILE: boringNotch/helpers/MediaChecker.swift ================================================ // // MediaChecker.swift // boringNotch // // Created by Alexander on 2025-07-26. // import Foundation final class MediaChecker: Sendable { enum MediaCheckerError: Error { case missingResources case processExecutionFailed case timeout } func checkDeprecationStatus() async throws -> Bool { try await Task.detached(priority: .userInitiated) { guard let scriptURL = Bundle.main.url(forResource: "mediaremote-adapter", withExtension: "pl"), let nowPlayingTestClientPath = Bundle.main.url(forResource: "MediaRemoteAdapterTestClient", withExtension: nil)?.path, let frameworkPath = Bundle.main.privateFrameworksPath?.appending("/MediaRemoteAdapter.framework") else { throw MediaCheckerError.missingResources } let process = Process() process.executableURL = URL(fileURLWithPath: "/usr/bin/perl") process.arguments = [scriptURL.path, frameworkPath, nowPlayingTestClientPath, "test"] do { try process.run() } catch { throw MediaCheckerError.processExecutionFailed } // Timeout after 10 seconds let didExit: Bool = try await withThrowingTaskGroup(of: Bool.self) { group in group.addTask { process.waitUntilExit() return true } group.addTask { try await Task.sleep(for: .seconds(10)) if process.isRunning { process.terminate() } return false // Timed out } for try await exited in group { if exited { group.cancelAll() return true } } throw MediaCheckerError.timeout } if !didExit { throw MediaCheckerError.timeout } let isDeprecated = process.terminationStatus == 1 return isDeprecated }.value } } ================================================ FILE: boringNotch/managers/BatteryActivityManager.swift ================================================ import Foundation import IOKit.ps /// Manages and monitors battery status changes on the device /// - Note: This class uses the IOKit framework to monitor battery status class BatteryActivityManager { static let shared = BatteryActivityManager() var onBatteryLevelChange: ((Float) -> Void)? var onMaxCapacityChange: ((Float) -> Void)? var onPowerModeChange: ((Bool) -> Void)? var onPowerSourceChange: ((Bool) -> Void)? var onChargingChange: ((Bool) -> Void)? var onTimeToFullChargeChange: ((Int) -> Void)? private var batterySource: CFRunLoopSource? private var observers: [(BatteryEvent) -> Void] = [] private var previousBatteryInfo: BatteryInfo? private var notificationQueue: [BatteryEvent] = [] private var isProcessingNotifications = false enum BatteryEvent { case powerSourceChanged(isPluggedIn: Bool) case batteryLevelChanged(level: Float) case lowPowerModeChanged(isEnabled: Bool) case isChargingChanged(isCharging: Bool) case timeToFullChargeChanged(time: Int) case maxCapacityChanged(capacity: Float) case error(description: String) } enum BatteryError: Error { case powerSourceUnavailable case batteryInfoUnavailable(String) case batteryParameterMissing(String) } private let defaultBatteryInfo = BatteryInfo( isPluggedIn: false, isCharging: false, currentCapacity: 0, maxCapacity: 0, isInLowPowerMode: false, timeToFullCharge: 0 ) private init() { startMonitoring() setupLowPowerModeObserver() } /// Setup observer for low power mode changes private func setupLowPowerModeObserver() { NotificationCenter.default.addObserver( self, selector: #selector(lowPowerModeChanged), name: NSNotification.Name.NSProcessInfoPowerStateDidChange, object: nil ) } /// Called when low power mode is enabled or disabled @objc private func lowPowerModeChanged() { notifyBatteryChanges() } /// Starts monitoring battery changes private func startMonitoring() { guard let powerSource = IOPSNotificationCreateRunLoopSource({ context in guard let context = context else { return } let manager = Unmanaged.fromOpaque(context).takeUnretainedValue() manager.notifyBatteryChanges() }, Unmanaged.passUnretained(self).toOpaque())?.takeRetainedValue() else { return } batterySource = powerSource CFRunLoopAddSource(CFRunLoopGetCurrent(), powerSource, .defaultMode) } /// Stops monitoring battery changes private func stopMonitoring() { if let powerSource = batterySource { CFRunLoopRemoveSource(CFRunLoopGetCurrent(), powerSource, .defaultMode) batterySource = nil } } /// Checks for changes in a property and notifies observers private func checkAndNotify( previous: T, current: T, eventGenerator: (T) -> BatteryEvent ) { if previous != current { enqueueNotification(eventGenerator(current)) } } /// Notifies the observers of battery changes /// Checks for changes in battery status and notifies observers private func notifyBatteryChanges() { let batteryInfo = getBatteryInfo() // Check for changes if let previousInfo = previousBatteryInfo { // Usar la función auxiliar para cada propiedad checkAndNotify( previous: previousInfo.isPluggedIn, current: batteryInfo.isPluggedIn, eventGenerator: { .powerSourceChanged(isPluggedIn: $0) } ) checkAndNotify( previous: previousInfo.currentCapacity, current: batteryInfo.currentCapacity, eventGenerator: { .batteryLevelChanged(level: $0) } ) checkAndNotify( previous: previousInfo.isCharging, current: batteryInfo.isCharging, eventGenerator: { .isChargingChanged(isCharging: $0) } ) checkAndNotify( previous: previousInfo.isInLowPowerMode, current: batteryInfo.isInLowPowerMode, eventGenerator: { .lowPowerModeChanged(isEnabled: $0) } ) checkAndNotify( previous: previousInfo.timeToFullCharge, current: batteryInfo.timeToFullCharge, eventGenerator: { .timeToFullChargeChanged(time: $0) } ) checkAndNotify( previous: previousInfo.maxCapacity, current: batteryInfo.maxCapacity, eventGenerator: { .maxCapacityChanged(capacity: $0) } ) } else { // First time notification enqueueNotification(.powerSourceChanged(isPluggedIn: batteryInfo.isPluggedIn)) enqueueNotification(.batteryLevelChanged(level: batteryInfo.currentCapacity)) enqueueNotification(.isChargingChanged(isCharging: batteryInfo.isCharging)) enqueueNotification(.lowPowerModeChanged(isEnabled: batteryInfo.isInLowPowerMode)) enqueueNotification(.timeToFullChargeChanged(time: batteryInfo.timeToFullCharge)) enqueueNotification(.maxCapacityChanged(capacity: batteryInfo.maxCapacity)) } // Update previous battery info previousBatteryInfo = batteryInfo // Trigger optional callbacks DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.onBatteryLevelChange?(batteryInfo.currentCapacity) self.onPowerSourceChange?(batteryInfo.isPluggedIn) self.onChargingChange?(batteryInfo.isCharging) self.onPowerModeChange?(batteryInfo.isInLowPowerMode) self.onTimeToFullChargeChange?(batteryInfo.timeToFullCharge) self.onMaxCapacityChange?(batteryInfo.maxCapacity) } } /// Enqueues a notification to be processed /// - Parameter event: The battery event private func enqueueNotification(_ event: BatteryEvent) { notificationQueue.append(event) processNextNotification() } /// Processes the next notification in the queue /// If there are no more notifications, the queue is cleared /// and the processing flag is set to false private func processNextNotification() { guard !isProcessingNotifications, !notificationQueue.isEmpty else { return } isProcessingNotifications = true let event = notificationQueue.removeFirst() DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in guard let self = self else { return } self.notifyObservers(event: event) self.isProcessingNotifications = false // Check if there are more items in the queue if !self.notificationQueue.isEmpty { self.processNextNotification() } } } /// Initializes the battery information when the manager starts /// - Returns: Current battery information func initializeBatteryInfo() -> BatteryInfo { previousBatteryInfo = getBatteryInfo() guard let batteryInfo = previousBatteryInfo else { return BatteryInfo( isPluggedIn: false, isCharging: false, currentCapacity: 0, maxCapacity: 0, isInLowPowerMode: false, timeToFullCharge: 0 ) } return batteryInfo } /// Get the current battery information /// - Returns: The current battery information private func getBatteryInfo() -> BatteryInfo { do { // Get power source information guard let snapshot = IOPSCopyPowerSourcesInfo()?.takeRetainedValue() else { throw BatteryError.powerSourceUnavailable } guard let sources = IOPSCopyPowerSourcesList(snapshot)?.takeRetainedValue() as? [CFTypeRef], !sources.isEmpty else { throw BatteryError.batteryInfoUnavailable("No power sources available") } let source = sources.first! guard let description = IOPSGetPowerSourceDescription(snapshot, source)?.takeUnretainedValue() as? [String: Any] else { throw BatteryError.batteryInfoUnavailable("Could not get power source description") } // Extract required battery parameters with error handling guard let currentCapacity = description[kIOPSCurrentCapacityKey] as? Float else { throw BatteryError.batteryParameterMissing("Current capacity") } guard let maxCapacity = description[kIOPSMaxCapacityKey] as? Float else { throw BatteryError.batteryParameterMissing("Max capacity") } guard let isCharging = description["Is Charging"] as? Bool else { throw BatteryError.batteryParameterMissing("Charging state") } guard let powerSource = description[kIOPSPowerSourceStateKey] as? String else { throw BatteryError.batteryParameterMissing("Power source state") } // Create battery info with the extracted parameters var batteryInfo = BatteryInfo( isPluggedIn: powerSource == kIOPSACPowerValue, isCharging: isCharging, currentCapacity: currentCapacity, maxCapacity: maxCapacity, isInLowPowerMode: ProcessInfo.processInfo.isLowPowerModeEnabled, timeToFullCharge: 0 ) // Optional parameters if let timeToFullCharge = description[kIOPSTimeToFullChargeKey] as? Int { batteryInfo.timeToFullCharge = timeToFullCharge } return batteryInfo } catch BatteryError.powerSourceUnavailable { print("⚠️ Error: Power source information unavailable") return defaultBatteryInfo } catch BatteryError.batteryInfoUnavailable(let reason) { print("⚠️ Error: Battery information unavailable - \(reason)") return defaultBatteryInfo } catch BatteryError.batteryParameterMissing(let parameter) { print("⚠️ Error: Battery parameter missing - \(parameter)") return defaultBatteryInfo } catch { print("⚠️ Error: Unexpected error getting battery info - \(error.localizedDescription)") return defaultBatteryInfo } } /// Adds an observer to listen to battery changes /// - Parameter observer: The observer closure to be called on battery events /// - Returns: The ID of the observer for later removal func addObserver(_ observer: @escaping (BatteryEvent) -> Void) -> Int { observers.append(observer) return observers.count - 1 } /// Removes an observer by its ID /// - Parameter id: The ID of the observer to be removed func removeObserver(byId id: Int) { guard id >= 0 && id < observers.count else { return } observers.remove(at: id) } /// Notifies all observers of a battery event /// - Parameter event: The battery event to notify private func notifyObservers(event: BatteryEvent) { DispatchQueue.main.async { [weak self] in guard let self = self else { return } for observer in self.observers { observer(event) } } } deinit { stopMonitoring() NotificationCenter.default.removeObserver(self) } } /// Struct to hold battery information struct BatteryInfo { var isPluggedIn: Bool var isCharging: Bool var currentCapacity: Float var maxCapacity: Float var isInLowPowerMode: Bool var timeToFullCharge: Int } ================================================ FILE: boringNotch/managers/BrightnessManager.swift ================================================ // BrightnessManager.swift // boringNotch // // Created by JeanLouis on 08/22/24. import AppKit final class BrightnessManager: ObservableObject { static let shared = BrightnessManager() @Published private(set) var rawBrightness: Float = 0 @Published private(set) var animatedBrightness: Float = 0 @Published private(set) var lastChangeAt: Date = .distantPast private let visibleDuration: TimeInterval = 1.2 private let client = XPCHelperClient.shared private init() { refresh() } var shouldShowOverlay: Bool { Date().timeIntervalSince(lastChangeAt) < visibleDuration } func refresh() { Task { @MainActor in if let current = await client.currentScreenBrightness() { publish(brightness: current, touchDate: false) } } } @MainActor func setRelative(delta: Float) { Task { @MainActor in let starting = await client.currentScreenBrightness() ?? rawBrightness let target = max(0, min(1, starting + delta)) let ok = await client.setScreenBrightness(target) if ok { publish(brightness: target, touchDate: true) } else { refresh() } BoringViewCoordinator.shared.toggleSneakPeek(status: true, type: .brightness, value: CGFloat(target)) } } func setAbsolute(value: Float) { let clamped = max(0, min(1, value)) Task { @MainActor in let ok = await client.setScreenBrightness(clamped) if ok { publish(brightness: clamped, touchDate: true) } else { refresh() } } } private func publish(brightness: Float, touchDate: Bool) { DispatchQueue.main.async { if self.rawBrightness != brightness || touchDate { if touchDate { self.lastChangeAt = Date() } self.rawBrightness = brightness self.animatedBrightness = brightness } } } } // (DisplayServices helpers moved into XPC helper) // MARK: - Keyboard Backlight Controller final class KeyboardBacklightManager: ObservableObject { static let shared = KeyboardBacklightManager() @Published private(set) var rawBrightness: Float = 0 @Published private(set) var lastChangeAt: Date = .distantPast private let visibleDuration: TimeInterval = 1.2 private let client = XPCHelperClient.shared private init() { refresh() } var shouldShowOverlay: Bool { Date().timeIntervalSince(lastChangeAt) < visibleDuration } func refresh() { Task { @MainActor in if let current = await client.currentKeyboardBrightness() { publish(brightness: current, touchDate: false) } } } @MainActor func setRelative(delta: Float) { Task { @MainActor in let starting = await client.currentKeyboardBrightness() ?? rawBrightness let target = max(0, min(1, starting + delta)) let ok = await client.setKeyboardBrightness(target) if ok { publish(brightness: target, touchDate: true) } else { refresh() } BoringViewCoordinator.shared.toggleSneakPeek( status: true, type: .backlight, value: CGFloat(target) ) } } func setAbsolute(value: Float) { let clamped = max(0, min(1, value)) Task { @MainActor in let ok = await client.setKeyboardBrightness(clamped) if ok { publish(brightness: clamped, touchDate: true) } else { refresh() } } } private func publish(brightness: Float, touchDate: Bool) { DispatchQueue.main.async { if self.rawBrightness != brightness || touchDate { if touchDate { self.lastChangeAt = Date() } self.rawBrightness = brightness } } } } ================================================ FILE: boringNotch/managers/CalendarManager.swift ================================================ // // CalendarManager.swift // boringNotch // // Created by Harsh Vardhan Goswami on 08/09/24. // import Defaults import EventKit import SwiftUI // MARK: - CalendarManager @MainActor class CalendarManager: ObservableObject { static let shared = CalendarManager() @Published var currentWeekStartDate: Date @Published var events: [EventModel] = [] @Published var allCalendars: [CalendarModel] = [] @Published var eventCalendars: [CalendarModel] = [] @Published var reminderLists: [CalendarModel] = [] @Published var selectedCalendarIDs: Set = [] @Published var calendarAuthorizationStatus: EKAuthorizationStatus = .notDetermined @Published var reminderAuthorizationStatus: EKAuthorizationStatus = .notDetermined private var selectedCalendars: [CalendarModel] = [] private let calendarService = CalendarService() private var eventStoreChangedObserver: NSObjectProtocol? private init() { self.currentWeekStartDate = CalendarManager.startOfDay(Date()) setupEventStoreChangedObserver() Task { await reloadCalendarAndReminderLists() } } deinit { if let observer = eventStoreChangedObserver { NotificationCenter.default.removeObserver(observer) } } private func setupEventStoreChangedObserver() { eventStoreChangedObserver = NotificationCenter.default.addObserver( forName: .EKEventStoreChanged, object: nil, queue: .main ) { [weak self] _ in Task { await self?.reloadCalendarAndReminderLists() } } } @MainActor func reloadCalendarAndReminderLists() async { let all = await calendarService.calendars() self.eventCalendars = all.filter { !$0.isReminder } self.reminderLists = all.filter { $0.isReminder } self.allCalendars = all // for legacy compatibility, can be removed if not needed updateSelectedCalendars() } func checkCalendarAuthorization() async { let status = EKEventStore.authorizationStatus(for: .event) DispatchQueue.main.async { print("📅 Current calendar authorization status: \(status)") self.calendarAuthorizationStatus = status } switch status { case .notDetermined: guard let granted = try? await calendarService.requestAccess(to: .event) else { self.calendarAuthorizationStatus = .notDetermined return } self.calendarAuthorizationStatus = granted ? .fullAccess : .denied if granted { await reloadCalendarAndReminderLists() events = await calendarService.events( from: currentWeekStartDate, to: Calendar.current.date(byAdding: .day, value: 1, to: currentWeekStartDate)!, calendars: selectedCalendars.map { $0.id }) } case .restricted, .denied: NSLog("Calendar access denied or restricted") case .fullAccess: NSLog("Full access") await reloadCalendarAndReminderLists() events = await calendarService.events( from: currentWeekStartDate, to: Calendar.current.date(byAdding: .day, value: 1, to: currentWeekStartDate)!, calendars: selectedCalendars.map { $0.id }) case .writeOnly: NSLog("Write only") @unknown default: print("Unknown authorization status") } } func checkReminderAuthorization() async { let status = EKEventStore.authorizationStatus(for: .reminder) DispatchQueue.main.async { print("📅 Current reminder authorization status: \(status)") self.reminderAuthorizationStatus = status } switch status { case .notDetermined: guard let granted = try? await calendarService.requestAccess(to: .reminder) else { self.reminderAuthorizationStatus = .notDetermined return } self.reminderAuthorizationStatus = granted ? .fullAccess : .denied if granted { await reloadCalendarAndReminderLists() } case .restricted, .denied: NSLog("Reminder access denied or restricted") case .fullAccess: NSLog("Full access") await reloadCalendarAndReminderLists() case .writeOnly: NSLog("Write only") @unknown default: print("Unknown authorization status") } } func updateSelectedCalendars() { // Populate selectedCalendarIDs based on Defaults calendar selection state switch Defaults[.calendarSelectionState] { case .all: selectedCalendarIDs = Set(allCalendars.map { $0.id }) case .selected(let identifiers): selectedCalendarIDs = identifiers } // Update the local calendar objects that correspond to the selected ids selectedCalendars = allCalendars.filter { selectedCalendarIDs.contains($0.id) } } func getCalendarSelected(_ calendar: CalendarModel) -> Bool { return selectedCalendarIDs.contains(calendar.id) } func setCalendarSelected(_ calendar: CalendarModel, isSelected: Bool) async { var selectionState = Defaults[.calendarSelectionState] switch selectionState { case .all: if !isSelected { let identifiers = Set(allCalendars.map { $0.id }).subtracting([calendar.id]) selectionState = .selected(identifiers) } case .selected(var identifiers): if isSelected { identifiers.insert(calendar.id) } else { identifiers.remove(calendar.id) } selectionState = identifiers.isEmpty ? .all : identifiers.count == allCalendars.count ? .all : .selected(identifiers) // if empty, select all } Defaults[.calendarSelectionState] = selectionState updateSelectedCalendars() await updateEvents() } static func startOfDay(_ date: Date) -> Date { return Calendar.current.startOfDay(for: date) } func updateCurrentDate(_ date: Date) async { currentWeekStartDate = Calendar.current.startOfDay(for: date) await updateEvents() } private func updateEvents() async { let calendarIDs = selectedCalendars.map { $0.id } let eventsResult = await calendarService.events( from: currentWeekStartDate, to: Calendar.current.date(byAdding: .day, value: 1, to: currentWeekStartDate)!, calendars: calendarIDs ) self.events = eventsResult } func setReminderCompleted(reminderID: String, completed: Bool) async { await calendarService.setReminderCompleted(reminderID: reminderID, completed: completed) // Refresh events after updating events = await calendarService.events( from: currentWeekStartDate, to: Calendar.current.date(byAdding: .day, value: 1, to: currentWeekStartDate)!, calendars: selectedCalendars.map { $0.id }) } } ================================================ FILE: boringNotch/managers/ImageService.swift ================================================ // // ImageService.swift // boringNotch // // Created by Alexander on 2025-09-13. // import Foundation import Defaults public protocol ImageServiceProtocol { func fetchImageData(from url: URL) async throws -> Data } public final class ImageService: ImageServiceProtocol { public static let shared = ImageService() private let session: URLSession private init() { let config = URLSessionConfiguration.default let cache = URLCache(memoryCapacity: 50 * 1024 * 1024, // 50MB diskCapacity: 100 * 1024 * 1024, // 100MB diskPath: "artwork_cache") config.urlCache = cache config.timeoutIntervalForRequest = 15 config.timeoutIntervalForResource = 30 config.httpShouldSetCookies = false self.session = URLSession(configuration: config) performLegacyCacheCleanupIfNeeded() } private func performLegacyCacheCleanupIfNeeded() { if !Defaults[.didClearLegacyURLCacheV1] { URLCache.shared.removeAllCachedResponses() Defaults[.didClearLegacyURLCacheV1] = true } } public func fetchImageData(from url: URL) async throws -> Data { guard let scheme = url.scheme?.lowercased(), scheme == "http" || scheme == "https" else { throw URLError(.unsupportedURL) } let (data, _) = try await session.data(from: url) return data } } ================================================ FILE: boringNotch/managers/MusicManager.swift ================================================ // // MusicManager.swift // boringNotch // // Created by Harsh Vardhan Goswami on 03/08/24. // import AppKit import Combine import Defaults import SwiftUI let defaultImage: NSImage = .init( systemSymbolName: "heart.fill", accessibilityDescription: "Album Art" )! class MusicManager: ObservableObject { // MARK: - Properties static let shared = MusicManager() private var cancellables = Set() private var controllerCancellables = Set() private var debounceIdleTask: Task? // Helper to check if macOS has removed support for NowPlayingController public private(set) var isNowPlayingDeprecated: Bool = false private let mediaChecker = MediaChecker() // Active controller private var activeController: (any MediaControllerProtocol)? // Published properties for UI @Published var songTitle: String = "I'm Handsome" @Published var artistName: String = "Me" @Published var albumArt: NSImage = defaultImage @Published var isPlaying = false @Published var album: String = "Self Love" @Published var isPlayerIdle: Bool = true @Published var animations: BoringAnimations = .init() @Published var avgColor: NSColor = .white @Published var bundleIdentifier: String? = nil @Published var songDuration: TimeInterval = 0 @Published var elapsedTime: TimeInterval = 0 @Published var timestampDate: Date = .init() @Published var playbackRate: Double = 1 @Published var isShuffled: Bool = false @Published var repeatMode: RepeatMode = .off @Published var volume: Double = 0.5 @Published var volumeControlSupported: Bool = true @ObservedObject var coordinator = BoringViewCoordinator.shared @Published var usingAppIconForArtwork: Bool = false @Published var currentLyrics: String = "" @Published var isFetchingLyrics: Bool = false @Published var syncedLyrics: [(time: Double, text: String)] = [] @Published var canFavoriteTrack: Bool = false @Published var isFavoriteTrack: Bool = false private var artworkData: Data? = nil // Store last values at the time artwork was changed private var lastArtworkTitle: String = "I'm Handsome" private var lastArtworkArtist: String = "Me" private var lastArtworkAlbum: String = "Self Love" private var lastArtworkBundleIdentifier: String? = nil @Published var isFlipping: Bool = false private var flipWorkItem: DispatchWorkItem? @Published var isTransitioning: Bool = false private var transitionWorkItem: DispatchWorkItem? // MARK: - Initialization init() { // Listen for changes to the default controller preference NotificationCenter.default.publisher(for: Notification.Name.mediaControllerChanged) .sink { [weak self] _ in self?.setActiveControllerBasedOnPreference() } .store(in: &cancellables) // Initialize deprecation check asynchronously Task { @MainActor in do { self.isNowPlayingDeprecated = try await self.mediaChecker.checkDeprecationStatus() print("Deprecation check completed: \(self.isNowPlayingDeprecated)") } catch { print("Failed to check deprecation status: \(error). Defaulting to false.") self.isNowPlayingDeprecated = false } // Initialize the active controller after deprecation check self.setActiveControllerBasedOnPreference() } } deinit { destroy() } public func destroy() { debounceIdleTask?.cancel() cancellables.removeAll() controllerCancellables.removeAll() flipWorkItem?.cancel() transitionWorkItem?.cancel() // Release active controller activeController = nil } // MARK: - Setup Methods private func createController(for type: MediaControllerType) -> (any MediaControllerProtocol)? { // Cleanup previous controller if activeController != nil { controllerCancellables.removeAll() activeController = nil } let newController: (any MediaControllerProtocol)? switch type { case .nowPlaying: // Only create NowPlayingController if not deprecated on this macOS version if !self.isNowPlayingDeprecated { newController = NowPlayingController() } else { return nil } case .appleMusic: newController = AppleMusicController() case .spotify: newController = SpotifyController() case .youtubeMusic: newController = YouTubeMusicController() } // Set up state observation for the new controller if let controller = newController { controller.playbackStatePublisher .receive(on: DispatchQueue.main) .sink { [weak self] state in guard let self = self, self.activeController === controller else { return } self.updateFromPlaybackState(state) } .store(in: &controllerCancellables) } return newController } private func setActiveControllerBasedOnPreference() { let preferredType = Defaults[.mediaController] print("Preferred Media Controller: \(preferredType)") // If NowPlaying is deprecated but that's the preference, use Apple Music instead let controllerType = (self.isNowPlayingDeprecated && preferredType == .nowPlaying) ? .appleMusic : preferredType if let controller = createController(for: controllerType) { setActiveController(controller) } else if controllerType != .appleMusic, let fallbackController = createController(for: .appleMusic) { // Fallback to Apple Music if preferred controller couldn't be created setActiveController(fallbackController) } } private func setActiveController(_ controller: any MediaControllerProtocol) { // Cancel any existing flip animation flipWorkItem?.cancel() // Set new active controller activeController = controller self.canFavoriteTrack = controller.supportsFavorite // Get current state from active controller forceUpdate() } // MARK: - Update Methods @MainActor private func updateFromPlaybackState(_ state: PlaybackState) { // Check for playback state changes (playing/paused) if state.isPlaying != self.isPlaying { NSLog("Playback state changed: \(state.isPlaying ? "Playing" : "Paused")") withAnimation(.smooth) { self.isPlaying = state.isPlaying self.updateIdleState(state: state.isPlaying) } if state.isPlaying && !state.title.isEmpty && !state.artist.isEmpty { self.updateSneakPeek() } } // Check for changes in track metadata using last artwork change values let titleChanged = state.title != self.lastArtworkTitle let artistChanged = state.artist != self.lastArtworkArtist let albumChanged = state.album != self.lastArtworkAlbum let bundleChanged = state.bundleIdentifier != self.lastArtworkBundleIdentifier // Check for artwork changes let artworkChanged = state.artwork != nil && state.artwork != self.artworkData let hasContentChange = titleChanged || artistChanged || albumChanged || artworkChanged || bundleChanged // Handle artwork and visual transitions for changed content if hasContentChange { self.triggerFlipAnimation() if artworkChanged, let artwork = state.artwork { self.updateArtwork(artwork) } else if state.artwork == nil { // Try to use app icon if no artwork but track changed if let appIconImage = AppIconAsNSImage(for: state.bundleIdentifier) { self.usingAppIconForArtwork = true self.updateAlbumArt(newAlbumArt: appIconImage) } } self.artworkData = state.artwork if artworkChanged || state.artwork == nil { // Update last artwork change values self.lastArtworkTitle = state.title self.lastArtworkArtist = state.artist self.lastArtworkAlbum = state.album self.lastArtworkBundleIdentifier = state.bundleIdentifier } // Only update sneak peek if there's actual content and something changed if !state.title.isEmpty && !state.artist.isEmpty && state.isPlaying { self.updateSneakPeek() } // Fetch lyrics on content change self.fetchLyricsIfAvailable(bundleIdentifier: state.bundleIdentifier, title: state.title, artist: state.artist) } let timeChanged = state.currentTime != self.elapsedTime let durationChanged = state.duration != self.songDuration let playbackRateChanged = state.playbackRate != self.playbackRate let shuffleChanged = state.isShuffled != self.isShuffled let repeatModeChanged = state.repeatMode != self.repeatMode let volumeChanged = state.volume != self.volume if state.title != self.songTitle { self.songTitle = state.title } if state.artist != self.artistName { self.artistName = state.artist } if state.album != self.album { self.album = state.album } if timeChanged { self.elapsedTime = state.currentTime } if durationChanged { self.songDuration = state.duration } if playbackRateChanged { self.playbackRate = state.playbackRate } if shuffleChanged { self.isShuffled = state.isShuffled } if state.bundleIdentifier != self.bundleIdentifier { self.bundleIdentifier = state.bundleIdentifier // Update volume control support from active controller self.volumeControlSupported = activeController?.supportsVolumeControl ?? false } if repeatModeChanged { self.repeatMode = state.repeatMode } if state.isFavorite != self.isFavoriteTrack { self.isFavoriteTrack = state.isFavorite } if volumeChanged { self.volume = state.volume } self.timestampDate = state.lastUpdated } func toggleFavoriteTrack() { guard canFavoriteTrack else { return } // Toggle based on current state setFavorite(!isFavoriteTrack) } @MainActor private func toggleAppleMusicFavorite() async { let runningApps = NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.Music") guard !runningApps.isEmpty else { return } let script = """ tell application \"Music\" if it is running then try set loved of current track to (not loved of current track) return loved of current track on error return false end try else return false end if end tell """ if let result = try? await AppleScriptHelper.execute(script) { let loved = result.booleanValue self.isFavoriteTrack = loved self.forceUpdate() } } func setFavorite(_ favorite: Bool) { guard canFavoriteTrack else { return } guard let controller = activeController else { return } Task { @MainActor in await controller.setFavorite(favorite) try? await Task.sleep(for: .milliseconds(150)) await controller.updatePlaybackInfo() } } /// Placeholder dislike function func dislikeCurrentTrack() { setFavorite(false) } // MARK: - Lyrics private func fetchLyricsIfAvailable(bundleIdentifier: String?, title: String, artist: String) { guard Defaults[.enableLyrics], !title.isEmpty else { DispatchQueue.main.async { self.isFetchingLyrics = false self.currentLyrics = "" } return } // Prefer native Apple Music lyrics when available if let bundleIdentifier = bundleIdentifier, bundleIdentifier.contains("com.apple.Music") { Task { @MainActor in let runningApps = NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.Music") guard !runningApps.isEmpty else { await self.fetchLyricsFromWeb(title: title, artist: artist) return } self.isFetchingLyrics = true self.currentLyrics = "" do { let script = """ tell application \"Music\" if it is running then if player state is playing or player state is paused then try set l to lyrics of current track if l is missing value then return \"\" else return l end if on error return \"\" end try else return \"\" end if else return \"\" end if end tell """ if let result = try await AppleScriptHelper.execute(script), let lyricsString = result.stringValue, !lyricsString.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { self.currentLyrics = lyricsString.trimmingCharacters(in: .whitespacesAndNewlines) self.isFetchingLyrics = false self.syncedLyrics = [] return } } catch { // fall through to web lookup } await self.fetchLyricsFromWeb(title: title, artist: artist) } } else { Task { @MainActor in self.isFetchingLyrics = true self.currentLyrics = "" await self.fetchLyricsFromWeb(title: title, artist: artist) } } } private func normalizedQuery(_ string: String) -> String { string .folding(options: .diacriticInsensitive, locale: .current) .replacingOccurrences(of: "\u{FFFD}", with: "") } @MainActor private func fetchLyricsFromWeb(title: String, artist: String) async { let cleanTitle = normalizedQuery(title) let cleanArtist = normalizedQuery(artist) guard let encodedTitle = cleanTitle.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let encodedArtist = cleanArtist.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { self.currentLyrics = "" self.isFetchingLyrics = false return } // LRCLIB simple search (no auth): https://lrclib.net/api/search?track_name=...&artist_name=... let urlString = "https://lrclib.net/api/search?track_name=\(encodedTitle)&artist_name=\(encodedArtist)" guard let url = URL(string: urlString) else { self.currentLyrics = "" self.isFetchingLyrics = false return } do { let (data, response) = try await URLSession.shared.data(from: url) guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { self.currentLyrics = "" self.isFetchingLyrics = false return } if let jsonArray = try JSONSerialization.jsonObject(with: data) as? [[String: Any]], let first = jsonArray.first { // Prefer plain lyrics (syncedLyrics may also be present) let plain = (first["plainLyrics"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" let synced = (first["syncedLyrics"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" let resolved = plain.isEmpty ? synced : plain self.currentLyrics = resolved self.isFetchingLyrics = false if !synced.isEmpty { self.syncedLyrics = self.parseLRC(synced) } else { self.syncedLyrics = [] } } else { self.currentLyrics = "" self.isFetchingLyrics = false self.syncedLyrics = [] } } catch { self.currentLyrics = "" self.isFetchingLyrics = false self.syncedLyrics = [] } } // MARK: - Synced lyrics helpers private func parseLRC(_ lrc: String) -> [(time: Double, text: String)] { var result: [(Double, String)] = [] lrc.split(separator: "\n").forEach { lineSub in let line = String(lineSub) // Match [mm:ss.xx] or [m:ss] let pattern = #"\[(\d{1,2}):(\d{2})(?:\.(\d{1,2}))?\]"# guard let regex = try? NSRegularExpression(pattern: pattern) else { return } let nsLine = line as NSString if let match = regex.firstMatch(in: line, range: NSRange(location: 0, length: nsLine.length)) { let minStr = nsLine.substring(with: match.range(at: 1)) let secStr = nsLine.substring(with: match.range(at: 2)) let csRange = match.range(at: 3) let centiStr = csRange.location != NSNotFound ? nsLine.substring(with: csRange) : "0" let minutes = Double(minStr) ?? 0 let seconds = Double(secStr) ?? 0 let centis = Double(centiStr) ?? 0 let time = minutes * 60 + seconds + centis / 100.0 let textStart = match.range.location + match.range.length let text = nsLine.substring(from: textStart).trimmingCharacters(in: .whitespaces) if !text.isEmpty { result.append((time, text)) } } } return result.sorted { $0.0 < $1.0 } } func lyricLine(at elapsed: Double) -> String { guard !syncedLyrics.isEmpty else { return currentLyrics } // Binary search for last line with time <= elapsed var low = 0 var high = syncedLyrics.count - 1 var idx = 0 while low <= high { let mid = (low + high) / 2 if syncedLyrics[mid].time <= elapsed { idx = mid low = mid + 1 } else { high = mid - 1 } } return syncedLyrics[idx].text } private func triggerFlipAnimation() { // Cancel any existing animation flipWorkItem?.cancel() // Create a new animation let workItem = DispatchWorkItem { [weak self] in self?.isFlipping = true DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { self?.isFlipping = false } } flipWorkItem = workItem DispatchQueue.main.async(execute: workItem) } private func updateArtwork(_ artworkData: Data) { DispatchQueue.global(qos: .userInitiated).async { [weak self] in guard let self = self else { return } if let artworkImage = NSImage(data: artworkData) { DispatchQueue.main.async { [weak self] in self?.usingAppIconForArtwork = false self?.updateAlbumArt(newAlbumArt: artworkImage) } } } } private func updateIdleState(state: Bool) { if state { isPlayerIdle = false debounceIdleTask?.cancel() } else { debounceIdleTask?.cancel() debounceIdleTask = Task { [weak self] in guard let self = self else { return } try? await Task.sleep(for: .seconds(Defaults[.waitInterval])) withAnimation { self.isPlayerIdle = !self.isPlaying } } } } private var workItem: DispatchWorkItem? func updateAlbumArt(newAlbumArt: NSImage) { workItem?.cancel() withAnimation(.smooth) { self.albumArt = newAlbumArt if Defaults[.coloredSpectrogram] { self.calculateAverageColor() } } } // MARK: - Playback Position Estimation public func estimatedPlaybackPosition(at date: Date = Date()) -> TimeInterval { guard isPlaying else { return min(elapsedTime, songDuration) } let timeDifference = date.timeIntervalSince(timestampDate) let estimated = elapsedTime + (timeDifference * playbackRate) return min(max(0, estimated), songDuration) } func calculateAverageColor() { albumArt.averageColor { [weak self] color in DispatchQueue.main.async { withAnimation(.smooth) { self?.avgColor = color ?? .white } } } } private func updateSneakPeek() { if isPlaying && Defaults[.enableSneakPeek] { if Defaults[.sneakPeekStyles] == .standard { coordinator.toggleSneakPeek(status: true, type: .music) } else { coordinator.toggleExpandingView(status: true, type: .music) } } } // MARK: - Public Methods for controlling playback func playPause() { Task { await activeController?.togglePlay() } } func play() { Task { await activeController?.play() } } func pause() { Task { await activeController?.pause() } } func toggleShuffle() { Task { await activeController?.toggleShuffle() } } func toggleRepeat() { Task { await activeController?.toggleRepeat() } } func togglePlay() { Task { await activeController?.togglePlay() } } func nextTrack() { Task { await activeController?.nextTrack() } } func previousTrack() { Task { await activeController?.previousTrack() } } func seek(to position: TimeInterval) { Task { await activeController?.seek(to: position) } } func skip(seconds: TimeInterval) { let newPos = min(max(0, elapsedTime + seconds), songDuration) seek(to: newPos) } func setVolume(to level: Double) { if let controller = activeController { Task { await controller.setVolume(level) } } } func openMusicApp() { guard let bundleID = bundleIdentifier else { print("Error: appBundleIdentifier is nil") return } let workspace = NSWorkspace.shared if let appURL = workspace.urlForApplication(withBundleIdentifier: bundleID) { let configuration = NSWorkspace.OpenConfiguration() workspace.openApplication(at: appURL, configuration: configuration) { (app, error) in if let error = error { print("Failed to launch app with bundle ID: \(bundleID), error: \(error)") } else { print("Launched app with bundle ID: \(bundleID)") } } } else { print("Failed to find app with bundle ID: \(bundleID)") } } func forceUpdate() { // Request immediate update from the active controller Task { [weak self] in if self?.activeController?.isActive() == true { if let youtubeController = self?.activeController as? YouTubeMusicController { await youtubeController.pollPlaybackState() } else { await self?.activeController?.updatePlaybackInfo() } } } } func syncVolumeFromActiveApp() async { // Check if bundle identifier is valid and if the app is actually running guard let bundleID = bundleIdentifier, !bundleID.isEmpty, NSWorkspace.shared.runningApplications.contains(where: { $0.bundleIdentifier == bundleID }) else { return } var script: String? if bundleID == "com.apple.Music" { script = """ tell application "Music" if it is running then get sound volume else return 50 end if end tell """ } else if bundleID == "com.spotify.client" { script = """ tell application "Spotify" if it is running then get sound volume else return 50 end if end tell """ } else { // For unsupported apps, don't sync volume return } if let volumeScript = script, let result = try? await AppleScriptHelper.execute(volumeScript) { let volumeValue = result.int32Value let currentVolume = Double(volumeValue) / 100.0 await MainActor.run { if abs(currentVolume - self.volume) > 0.01 { self.volume = currentVolume } } } } } ================================================ FILE: boringNotch/managers/NotchSpaceManager.swift ================================================ // // NotchSpaceManager.swift // boringNotch // // Created by Alexander on 2024-10-27. // import Foundation class NotchSpaceManager { static let shared = NotchSpaceManager() let notchSpace: CGSSpace private var eventTap: CFMachPort? private var runLoopSource: CFRunLoopSource? private init() { notchSpace = CGSSpace(level: 2147483647) // Max level } } ================================================ FILE: boringNotch/managers/VolumeManager.swift ================================================ // // VolumeManager.swift // boringNotch // // Created by JeanLouis on 22/08/2025. // import AppKit import Combine import CoreAudio import Foundation final class VolumeManager: NSObject, ObservableObject { static let shared = VolumeManager() @Published private(set) var rawVolume: Float = 0 @Published private(set) var isMuted: Bool = false @Published private(set) var lastChangeAt: Date = .distantPast let visibleDuration: TimeInterval = 1.2 private var didInitialFetch = false private let step: Float32 = 1.0 / 16.0 // Fallback software if hardware mute is not supported private var previousVolumeBeforeMute: Float32 = 0.2 private var softwareMuted: Bool = false private override init() { super.init() setupAudioListener() fetchCurrentVolume() } var shouldShowOverlay: Bool { Date().timeIntervalSince(lastChangeAt) < visibleDuration } // MARK: - Public Control API @MainActor func increase(stepDivisor: Float = 1.0) { let divisor = max(stepDivisor, 0.25) let delta = step / Float32(divisor) let current = readVolumeInternal() ?? rawVolume let target = max(0, min(1, current + delta)) setAbsolute(target) BoringViewCoordinator.shared.toggleSneakPeek(status: true, type: .volume, value: CGFloat(target)) } @MainActor func decrease(stepDivisor: Float = 1.0) { let divisor = max(stepDivisor, 0.25) let delta = step / Float32(divisor) let current = readVolumeInternal() ?? rawVolume let target = max(0, min(1, current - delta)) setAbsolute(target) BoringViewCoordinator.shared.toggleSneakPeek(status: true, type: .volume, value: CGFloat(target)) } @MainActor func toggleMuteAction() { // Determine expected resulting state immediately and show HUD with that value let deviceID = systemOutputDeviceID() var willBeMuted = false var resultingVolume: Float32 = rawVolume if deviceID == kAudioObjectUnknown { willBeMuted = !softwareMuted resultingVolume = willBeMuted ? 0 : previousVolumeBeforeMute } else { let currentMuted = isMutedInternal() willBeMuted = !currentMuted resultingVolume = willBeMuted ? 0 : (readVolumeInternal() ?? rawVolume) } toggleMuteInternal() BoringViewCoordinator.shared.toggleSneakPeek(status: true, type: .volume, value: CGFloat(willBeMuted ? 0 : resultingVolume)) } func refresh() { fetchCurrentVolume() } func adjustRelative(delta: Float32) { if isMutedInternal() { toggleMuteInternal() } guard let current = readVolumeInternal() else { fetchCurrentVolume() return } let target = max(0, min(1, current + delta)) writeVolumeInternal(target) publish(volume: target, muted: isMutedInternal(), touchDate: true) } @MainActor func setAbsolute(_ value: Float32) { let clamped = max(0, min(1, value)) let currentlyMuted = isMutedInternal() if currentlyMuted && clamped > 0 { toggleMuteInternal() } writeVolumeInternal(clamped) if clamped == 0 && !currentlyMuted { toggleMuteInternal() } publish(volume: clamped, muted: isMutedInternal(), touchDate: true) } // MARK: - CoreAudio Helpers private func systemOutputDeviceID() -> AudioObjectID { var defaultDeviceID = kAudioObjectUnknown var propertyAddress = AudioObjectPropertyAddress( mSelector: kAudioHardwarePropertyDefaultOutputDevice, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain ) var dataSize = UInt32(MemoryLayout.size) let status = AudioObjectGetPropertyData( AudioObjectID(kAudioObjectSystemObject), &propertyAddress, 0, nil, &dataSize, &defaultDeviceID ) if status != noErr { return kAudioObjectUnknown } return defaultDeviceID } private func fetchCurrentVolume() { let deviceID = systemOutputDeviceID() guard deviceID != kAudioObjectUnknown else { return } var volumes: [Float32] = [] let candidateElements: [UInt32] = [kAudioObjectPropertyElementMain, 1, 2, 3, 4] for element in candidateElements { if let v = readValidatedScalar(deviceID: deviceID, element: element) { volumes.append(v) } } if !volumes.isEmpty { let avg = max(0, min(1, volumes.reduce(0, +) / Float32(volumes.count))) DispatchQueue.main.async { if self.rawVolume != avg { if self.didInitialFetch { self.lastChangeAt = Date() } } self.rawVolume = avg self.didInitialFetch = true } } var muteAddr = AudioObjectPropertyAddress( mSelector: kAudioDevicePropertyMute, mScope: kAudioDevicePropertyScopeOutput, mElement: kAudioObjectPropertyElementMain ) if AudioObjectHasProperty(deviceID, &muteAddr) { var sizeNeeded: UInt32 = 0 if AudioObjectGetPropertyDataSize(deviceID, &muteAddr, 0, nil, &sizeNeeded) == noErr, sizeNeeded == UInt32(MemoryLayout.size) { var muted: UInt32 = 0 var mSize = sizeNeeded if AudioObjectGetPropertyData(deviceID, &muteAddr, 0, nil, &mSize, &muted) == noErr { let newMuted = muted != 0 DispatchQueue.main.async { if self.isMuted != newMuted { self.lastChangeAt = Date() } self.isMuted = newMuted } } } } } private func setupAudioListener() { let deviceID = systemOutputDeviceID() guard deviceID != kAudioObjectUnknown else { return } var defaultDevAddr = AudioObjectPropertyAddress( mSelector: kAudioHardwarePropertyDefaultOutputDevice, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain ) AudioObjectAddPropertyListenerBlock( AudioObjectID(kAudioObjectSystemObject), &defaultDevAddr, nil ) { _, _ in self.fetchCurrentVolume() } var masterAddr = AudioObjectPropertyAddress( mSelector: kAudioDevicePropertyVolumeScalar, mScope: kAudioDevicePropertyScopeOutput, mElement: kAudioObjectPropertyElementMain ) if AudioObjectHasProperty(deviceID, &masterAddr) { AudioObjectAddPropertyListenerBlock(deviceID, &masterAddr, nil) { _, _ in self.fetchCurrentVolume() } } else { for ch in [UInt32(1), UInt32(2)] { var chAddr = AudioObjectPropertyAddress( mSelector: kAudioDevicePropertyVolumeScalar, mScope: kAudioDevicePropertyScopeOutput, mElement: ch ) if AudioObjectHasProperty(deviceID, &chAddr) { AudioObjectAddPropertyListenerBlock(deviceID, &chAddr, nil) { _, _ in self.fetchCurrentVolume() } } } } // Mute var muteAddr = AudioObjectPropertyAddress( mSelector: kAudioDevicePropertyMute, mScope: kAudioDevicePropertyScopeOutput, mElement: kAudioObjectPropertyElementMain ) if AudioObjectHasProperty(deviceID, &muteAddr) { AudioObjectAddPropertyListenerBlock(deviceID, &muteAddr, nil) { _, _ in self.fetchCurrentVolume() } } } private func readVolumeInternal() -> Float32? { let deviceID = systemOutputDeviceID() if deviceID == kAudioObjectUnknown { return nil } var collected: [Float32] = [] for el in [kAudioObjectPropertyElementMain, 1, 2, 3, 4] { if let v = readValidatedScalar(deviceID: deviceID, element: el) { collected.append(v) } } guard !collected.isEmpty else { return nil } return collected.reduce(0, +) / Float32(collected.count) } private func writeVolumeInternal(_ value: Float32) { let deviceID = systemOutputDeviceID() if deviceID == kAudioObjectUnknown { return } let newVal = max(0, min(1, value)) var written = false if writeValidatedScalar( deviceID: deviceID, element: kAudioObjectPropertyElementMain, value: newVal) { written = true } else { var any = false for el in [UInt32](1...4) { if writeValidatedScalar(deviceID: deviceID, element: el, value: newVal) { any = true } } written = any } if !written { // silent fail } } private func isMutedInternal() -> Bool { let deviceID = systemOutputDeviceID() if deviceID == kAudioObjectUnknown { return softwareMuted } var muteAddr = AudioObjectPropertyAddress( mSelector: kAudioDevicePropertyMute, mScope: kAudioDevicePropertyScopeOutput, mElement: kAudioObjectPropertyElementMain ) guard AudioObjectHasProperty(deviceID, &muteAddr) else { return softwareMuted } var sizeNeeded: UInt32 = 0 guard AudioObjectGetPropertyDataSize(deviceID, &muteAddr, 0, nil, &sizeNeeded) == noErr, sizeNeeded == UInt32(MemoryLayout.size) else { return softwareMuted } var muted: UInt32 = 0 var size = sizeNeeded if AudioObjectGetPropertyData(deviceID, &muteAddr, 0, nil, &size, &muted) == noErr { return muted != 0 } return softwareMuted } private func toggleMuteInternal() { let deviceID = systemOutputDeviceID() if deviceID == kAudioObjectUnknown { performSoftwareMuteToggle(currentVolume: rawVolume) return } var muteAddr = AudioObjectPropertyAddress( mSelector: kAudioDevicePropertyMute, mScope: kAudioDevicePropertyScopeOutput, mElement: kAudioObjectPropertyElementMain ) if !AudioObjectHasProperty(deviceID, &muteAddr) { let currentVol = readVolumeInternal() ?? rawVolume performSoftwareMuteToggle(currentVolume: currentVol) return } var sizeNeeded: UInt32 = 0 guard AudioObjectGetPropertyDataSize(deviceID, &muteAddr, 0, nil, &sizeNeeded) == noErr, sizeNeeded == UInt32(MemoryLayout.size) else { let currentVol = readVolumeInternal() ?? rawVolume performSoftwareMuteToggle(currentVolume: currentVol) return } var muted: UInt32 = 0 var size = sizeNeeded if AudioObjectGetPropertyData(deviceID, &muteAddr, 0, nil, &size, &muted) == noErr { var newVal: UInt32 = muted == 0 ? 1 : 0 AudioObjectSetPropertyData(deviceID, &muteAddr, 0, nil, size, &newVal) let vol = readVolumeInternal() ?? rawVolume publish(volume: vol, muted: newVal != 0, touchDate: true) } else { let currentVol = readVolumeInternal() ?? rawVolume performSoftwareMuteToggle(currentVolume: currentVol) } } private func performSoftwareMuteToggle(currentVolume: Float32) { if softwareMuted { let restore = max(0, min(1, previousVolumeBeforeMute)) writeVolumeInternal(restore) softwareMuted = false publish(volume: restore, muted: false, touchDate: true) } else { if currentVolume > 0.001 { previousVolumeBeforeMute = currentVolume } writeVolumeInternal(0) softwareMuted = true publish(volume: 0, muted: true, touchDate: true) } } private func readValidatedScalar(deviceID: AudioObjectID, element: UInt32) -> Float32? { var addr = AudioObjectPropertyAddress( mSelector: kAudioDevicePropertyVolumeScalar, mScope: kAudioDevicePropertyScopeOutput, mElement: element ) guard AudioObjectHasProperty(deviceID, &addr) else { return nil } var sizeNeeded: UInt32 = 0 guard AudioObjectGetPropertyDataSize(deviceID, &addr, 0, nil, &sizeNeeded) == noErr, sizeNeeded == UInt32(MemoryLayout.size) else { return nil } var vol = Float32(0) var size = sizeNeeded let status = AudioObjectGetPropertyData(deviceID, &addr, 0, nil, &size, &vol) return status == noErr ? vol : nil } private func writeValidatedScalar(deviceID: AudioObjectID, element: UInt32, value: Float32) -> Bool { var addr = AudioObjectPropertyAddress( mSelector: kAudioDevicePropertyVolumeScalar, mScope: kAudioDevicePropertyScopeOutput, mElement: element ) guard AudioObjectHasProperty(deviceID, &addr) else { return false } var sizeNeeded: UInt32 = 0 guard AudioObjectGetPropertyDataSize(deviceID, &addr, 0, nil, &sizeNeeded) == noErr, sizeNeeded == UInt32(MemoryLayout.size) else { return false } var val = value return AudioObjectSetPropertyData(deviceID, &addr, 0, nil, sizeNeeded, &val) == noErr } private func publish(volume: Float32, muted: Bool, touchDate: Bool) { DispatchQueue.main.async { if touchDate { self.lastChangeAt = Date() } self.rawVolume = volume self.isMuted = muted } } } extension Array where Element == Float32 { fileprivate var average: Float32? { isEmpty ? nil : reduce(0, +) / Float32(count) } } ================================================ FILE: boringNotch/managers/WebcamManager.swift ================================================ // // WebcamManager.swift // boringNotch // // Created by Harsh Vardhan Goswami on 19/08/24. // import AVFoundation import SwiftUI class WebcamManager: NSObject, ObservableObject { static let shared = WebcamManager() @Published var previewLayer: AVCaptureVideoPreviewLayer? { didSet { objectWillChange.send() } } private var captureSession: AVCaptureSession? @Published var isSessionRunning: Bool = false { didSet { objectWillChange.send() } } @Published var authorizationStatus: AVAuthorizationStatus = .notDetermined { didSet { objectWillChange.send() } } @Published var cameraAvailable: Bool = false { didSet { objectWillChange.send() } } private let sessionQueue = DispatchQueue(label: "BoringNotch.WebcamManager.SessionQueue", qos: .userInitiated) private var isCleaningUp: Bool = false // MARK: - Constants enum WebcamError: Error, LocalizedError { case deviceUnavailable case accessDenied case configurationFailed(String) var errorDescription: String? { switch self { case .deviceUnavailable: return "No camera devices available" case .accessDenied: return "Camera access denied" case .configurationFailed(let message): return "Camera configuration failed: \(message)" } } } // MARK: - Properties private override init() { super.init() NotificationCenter.default.addObserver(self, selector: #selector(deviceWasDisconnected), name: .AVCaptureDeviceWasDisconnected, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(deviceWasConnected), name: .AVCaptureDeviceWasConnected, object: nil) checkCameraAvailability() } deinit { NotificationCenter.default.removeObserver(self) if let session = captureSession { if session.isRunning { session.stopRunning() } } captureSession = nil previewLayer = nil } // MARK: - Camera Management /// Checks current authorization status and requests access if needed func checkAndRequestVideoAuthorization() { let status = AVCaptureDevice.authorizationStatus(for: .video) DispatchQueue.main.async { self.authorizationStatus = status } switch status { case .authorized: checkCameraAvailability() // Check availability if authorized case .notDetermined: requestVideoAccess() case .denied, .restricted: NSLog("Camera access denied or restricted") @unknown default: NSLog("Unknown authorization status") } } /// Requests access to the camera private func requestVideoAccess() { AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in DispatchQueue.main.async { self?.authorizationStatus = granted ? .authorized : .denied if granted { self?.checkCameraAvailability() // Check availability if access granted } } } } /// Checks if any camera devices are available and sets up capture session if needed func checkCameraAvailability() { let availableDevices = AVCaptureDevice.DiscoverySession( deviceTypes: [.external, .builtInWideAngleCamera], mediaType: .video, position: .unspecified ).devices let hasAvailableDevices = !availableDevices.isEmpty DispatchQueue.main.async { self.cameraAvailable = hasAvailableDevices } } /// Sets up the capture session with a completion handler private func setupCaptureSession(completion: @escaping (Bool) -> Void) { sessionQueue.async { [weak self] in guard let self = self else { completion(false) return } // Clean up any existing session before creating a new one self.cleanupExistingSession() let session = AVCaptureSession() do { // Get available devices and prefer external camera if available let discoverySession = AVCaptureDevice.DiscoverySession( deviceTypes: [.external, .builtInWideAngleCamera], mediaType: .video, position: .unspecified ) guard let videoDevice = discoverySession.devices.first else { NSLog("No video devices available") DispatchQueue.main.async { self.isSessionRunning = false self.cameraAvailable = false } completion(false) return } NSLog("Using camera: \(videoDevice.localizedName)") // Lock device for configuration try videoDevice.lockForConfiguration() defer { videoDevice.unlockForConfiguration() } let videoInput = try AVCaptureDeviceInput(device: videoDevice) guard session.canAddInput(videoInput) else { throw NSError(domain: "BoringNotch.WebcamManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "Cannot add video input"]) } session.beginConfiguration() session.sessionPreset = .high session.addInput(videoInput) let videoOutput = AVCaptureVideoDataOutput() videoOutput.setSampleBufferDelegate(nil, queue: nil) if session.canAddOutput(videoOutput) { session.addOutput(videoOutput) } session.commitConfiguration() self.captureSession = session // Create and set up preview layer on main thread DispatchQueue.main.async { self.cameraAvailable = true let previewLayer = AVCaptureVideoPreviewLayer(session: session) previewLayer.videoGravity = .resizeAspectFill self.previewLayer = previewLayer // Setup is complete, let the caller know completion(true) } NSLog("Capture session setup completed successfully") } catch { NSLog("Failed to setup capture session: \(error.localizedDescription)") DispatchQueue.main.async { self.isSessionRunning = false self.cameraAvailable = false self.previewLayer = nil } completion(false) } } } /// Cleans up an existing capture session, removing all inputs and outputs private func cleanupExistingSession() { if let existingSession = self.captureSession { // First stop the session if running if existingSession.isRunning { existingSession.stopRunning() } // Then perform configuration cleanup existingSession.beginConfiguration() // Remove all inputs and outputs for input in existingSession.inputs { existingSession.removeInput(input) } for output in existingSession.outputs { existingSession.removeOutput(output) } existingSession.commitConfiguration() self.captureSession = nil // Clear preview layer on main thread DispatchQueue.main.async { self.previewLayer = nil } } } @objc private func deviceWasDisconnected(notification: Notification) { NSLog("Camera device was disconnected") sessionQueue.async { [weak self] in guard let self = self else { return } self.stopSession() DispatchQueue.main.async { self.cameraAvailable = false } } } @objc private func deviceWasConnected(notification: Notification) { NSLog("Camera device was connected") sessionQueue.async { [weak self] in guard let self = self else { return } self.checkCameraAvailability() } } private func updateSessionState() { let isRunning = self.captureSession?.isRunning ?? false DispatchQueue.main.async { self.isSessionRunning = isRunning } } func startSession() { sessionQueue.async { [weak self] in guard let self = self else { return } // If no session exists, create new session if self.captureSession == nil { self.setupCaptureSession { success in if success { // Only start the session if setup was successful self.startRunningCaptureSession() } } } else { // Session already exists, just start it self.startRunningCaptureSession() } } } private func startRunningCaptureSession() { sessionQueue.async { [weak self] in guard let self = self, let session = self.captureSession, !session.isRunning else { return } session.startRunning() // Update state on main thread self.updateSessionState() NSLog("Capture session started successfully") } } func stopSession() { sessionQueue.async { [weak self] in guard let self = self else { return } // Update state to indicate we're stopping DispatchQueue.main.async { self.isSessionRunning = false } self.cleanupExistingSession() NSLog("Capture session stopped and cleaned up") } } } ================================================ FILE: boringNotch/menu/StatusBarMenu.swift ================================================ import Cocoa class BoringStatusMenu: NSMenu { var statusItem: NSStatusItem! override init() { super.init() // Initialize the status item statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) if let button = statusItem.button { button.image = NSImage(systemSymbolName: "music.note", accessibilityDescription: "BoringNotch") button.action = #selector(showMenu) } // Set up the menu let menu = NSMenu() menu.addItem(NSMenuItem(title: "Quit", action: #selector(quitAction), keyEquivalent: "q")) statusItem.menu = menu } } ================================================ FILE: boringNotch/metal/visualizer.metal ================================================ // // visualizer.metal // boringNotch // // Created by Harsh Vardhan Goswami on 28/08/24. // #include using namespace metal; vertex float4 vertexShader(uint vertexID [[vertex_id]], constant float2 *vertices [[buffer(0)]]) { return float4(vertices[vertexID], 0, 1); } fragment float4 fragmentShader(constant float4 &color [[buffer(0)]]) { return color; } ================================================ FILE: boringNotch/models/BatteryStatusViewModel.swift ================================================ import Cocoa import Defaults import Foundation import IOKit.ps import SwiftUI /// A view model that manages and monitors the battery status of the device class BatteryStatusViewModel: ObservableObject { private var wasCharging: Bool = false private var powerSourceChangedCallback: IOPowerSourceCallbackType? private var runLoopSource: Unmanaged? @ObservedObject var coordinator = BoringViewCoordinator.shared @Published private(set) var levelBattery: Float = 0.0 @Published private(set) var maxCapacity: Float = 0.0 @Published private(set) var isPluggedIn: Bool = false @Published private(set) var isCharging: Bool = false @Published private(set) var isInLowPowerMode: Bool = false @Published private(set) var isInitial: Bool = false @Published private(set) var timeToFullCharge: Int = 0 @Published private(set) var statusText: String = "" private let managerBattery = BatteryActivityManager.shared private var managerBatteryId: Int? static let shared = BatteryStatusViewModel() /// Initializes the view model with a given BoringViewModel instance /// - Parameter vm: The BoringViewModel instance private init() { setupPowerStatus() setupMonitor() } /// Sets up the initial power status by fetching battery information private func setupPowerStatus() { let batteryInfo = managerBattery.initializeBatteryInfo() updateBatteryInfo(batteryInfo) } /// Sets up the monitor to observe battery events private func setupMonitor() { managerBatteryId = managerBattery.addObserver { [weak self] event in guard let self = self else { return } self.handleBatteryEvent(event) } } /// Handles battery events and updates the corresponding properties /// - Parameter event: The battery event to handle private func handleBatteryEvent(_ event: BatteryActivityManager.BatteryEvent) { switch event { case .powerSourceChanged(let isPluggedIn): print("🔌 Power source: \(isPluggedIn ? "Connected" : "Disconnected")") withAnimation { self.isPluggedIn = isPluggedIn self.statusText = isPluggedIn ? "Plugged In" : "Unplugged" self.notifyImportanChangeStatus() } case .batteryLevelChanged(let level): print("🔋 Battery level: \(Int(level))%") withAnimation { self.levelBattery = level } case .lowPowerModeChanged(let isEnabled): print("⚡ Low power mode: \(isEnabled ? "Enabled" : "Disabled")") self.notifyImportanChangeStatus() withAnimation { self.isInLowPowerMode = isEnabled self.statusText = "Low Power: \(self.isInLowPowerMode ? "On" : "Off")" } case .isChargingChanged(let isCharging): print("🔌 Charging: \(isCharging ? "Yes" : "No")") print("maxCapacity: \(self.maxCapacity)") print("levelBattery: \(self.levelBattery)") self.notifyImportanChangeStatus() withAnimation { self.isCharging = isCharging self.statusText = isCharging ? "Charging battery" : (self.levelBattery < self.maxCapacity ? "Not charging" : "Full charge") } case .timeToFullChargeChanged(let time): print("🕒 Time to full charge: \(time) minutes") withAnimation { self.timeToFullCharge = time } case .maxCapacityChanged(let capacity): print("🔋 Max capacity: \(capacity)") withAnimation { self.maxCapacity = capacity } case .error(let description): print("⚠️ Error: \(description)") } } /// Updates the battery information with the given BatteryInfo instance /// - Parameter batteryInfo: The BatteryInfo instance containing the battery data private func updateBatteryInfo(_ batteryInfo: BatteryInfo) { withAnimation { self.levelBattery = batteryInfo.currentCapacity self.isPluggedIn = batteryInfo.isPluggedIn self.isCharging = batteryInfo.isCharging self.isInLowPowerMode = batteryInfo.isInLowPowerMode self.timeToFullCharge = batteryInfo.timeToFullCharge self.maxCapacity = batteryInfo.maxCapacity self.statusText = batteryInfo.isPluggedIn ? "Plugged In" : "Unplugged" } } /// Notifies important changes in the battery status with an optional delay /// - Parameter delay: The delay before notifying the change, default is 0.0 private func notifyImportanChangeStatus(delay: Double = 0.0) { Task { try? await Task.sleep(for: .seconds(delay)) self.coordinator.toggleExpandingView(status: true, type: .battery) } } deinit { print("🔌 Cleaning up battery monitoring...") if let managerBatteryId: Int = managerBatteryId { managerBattery.removeObserver(byId: managerBatteryId) } } } ================================================ FILE: boringNotch/models/BoringViewModel.swift ================================================ // // BoringViewModel.swift // boringNotch // // Created by Harsh Vardhan Goswami on 04/08/24. // import Combine import Defaults import SwiftUI class BoringViewModel: NSObject, ObservableObject { @ObservedObject var coordinator = BoringViewCoordinator.shared @ObservedObject var detector = FullscreenMediaDetector.shared let animationLibrary: BoringAnimations = .init() let animation: Animation? @Published var contentType: ContentType = .normal @Published private(set) var notchState: NotchState = .closed @Published var dragDetectorTargeting: Bool = false @Published var generalDropTargeting: Bool = false @Published var dropZoneTargeting: Bool = false @Published var dropEvent: Bool = false @Published var anyDropZoneTargeting: Bool = false var cancellables: Set = [] @Published var hideOnClosed: Bool = true @Published var edgeAutoOpenActive: Bool = false @Published var isHoveringCalendar: Bool = false @Published var isBatteryPopoverActive: Bool = false @Published var screenUUID: String? @Published var notchSize: CGSize = getClosedNotchSize() @Published var closedNotchSize: CGSize = getClosedNotchSize() let webcamManager = WebcamManager.shared @Published var isCameraExpanded: Bool = false @Published var isRequestingAuthorization: Bool = false deinit { destroy() } func destroy() { cancellables.forEach { $0.cancel() } cancellables.removeAll() } init(screenUUID: String? = nil) { animation = animationLibrary.animation super.init() self.screenUUID = screenUUID notchSize = getClosedNotchSize(screenUUID: screenUUID) closedNotchSize = notchSize Publishers.CombineLatest3($dropZoneTargeting, $dragDetectorTargeting, $generalDropTargeting) .map { shelf, drag, general in shelf || drag || general } .assign(to: \.anyDropZoneTargeting, on: self) .store(in: &cancellables) setupDetectorObserver() } private func setupDetectorObserver() { // Publisher for the user’s fullscreen detection setting let enabledPublisher = Defaults .publisher(.hideNotchOption) .map(\.newValue) .map { $0 != .never } .removeDuplicates() // Publisher for the current screen UUID (non-nil, distinct) let screenPublisher = $screenUUID .compactMap { $0 } .removeDuplicates() // Publisher for fullscreen status dictionary let fullscreenStatusPublisher = detector.$fullscreenStatus .removeDuplicates() // Combine all three: screen UUID, fullscreen status, and enabled setting Publishers.CombineLatest3(screenPublisher, fullscreenStatusPublisher, enabledPublisher) .map { screenUUID, fullscreenStatus, enabled in let isFullscreen = fullscreenStatus[screenUUID] ?? false return enabled && isFullscreen } .removeDuplicates() .receive(on: RunLoop.main) .sink { [weak self] shouldHide in withAnimation(.smooth) { self?.hideOnClosed = shouldHide } } .store(in: &cancellables) } // Computed property for effective notch height var effectiveClosedNotchHeight: CGFloat { let currentScreen = screenUUID.flatMap { NSScreen.screen(withUUID: $0) } let noNotchAndFullscreen = hideOnClosed && (currentScreen?.safeAreaInsets.top ?? 0 <= 0 || currentScreen == nil) return noNotchAndFullscreen ? 0 : closedNotchSize.height } var chinHeight: CGFloat { if !Defaults[.hideTitleBar] { return 0 } guard let currentScreen = screenUUID.flatMap({ NSScreen.screen(withUUID: $0) }) else { return 0 } if notchState == .open { return 0 } let menuBarHeight = currentScreen.frame.maxY - currentScreen.visibleFrame.maxY let currentHeight = effectiveClosedNotchHeight if currentHeight == 0 { return 0 } return max(0, menuBarHeight - currentHeight) } func toggleCameraPreview() { if isRequestingAuthorization { return } switch webcamManager.authorizationStatus { case .authorized: if webcamManager.isSessionRunning { webcamManager.stopSession() isCameraExpanded = false } else if webcamManager.cameraAvailable { webcamManager.startSession() isCameraExpanded = true } case .denied, .restricted: DispatchQueue.main.async { NSApp.setActivationPolicy(.regular) NSApp.activate(ignoringOtherApps: true) let alert = NSAlert() alert.messageText = "Camera Access Required" alert.informativeText = "Please allow camera access in System Settings." alert.addButton(withTitle: "Open Settings") alert.addButton(withTitle: "Cancel") if alert.runModal() == .alertFirstButtonReturn { if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Camera") { NSWorkspace.shared.open(url) } } NSApp.setActivationPolicy(.accessory) NSApp.deactivate() } case .notDetermined: isRequestingAuthorization = true webcamManager.checkAndRequestVideoAuthorization() DispatchQueue.main.asyncAfter(deadline: .now() + 2) { self.isRequestingAuthorization = false } default: break } } func isMouseHovering(position: NSPoint = NSEvent.mouseLocation) -> Bool { let screenFrame = getScreenFrame(screenUUID) if let frame = screenFrame { let baseY = frame.maxY - notchSize.height let baseX = frame.midX - notchSize.width / 2 return position.y >= baseY && position.x >= baseX && position.x <= baseX + notchSize.width } return false } func open() { self.notchSize = openNotchSize self.notchState = .open // Force music information update when notch is opened MusicManager.shared.forceUpdate() } func close() { // Do not close while a share picker or sharing service is active if SharingStateManager.shared.preventNotchClose { return } self.notchSize = getClosedNotchSize(screenUUID: self.screenUUID) self.closedNotchSize = self.notchSize self.notchState = .closed self.isBatteryPopoverActive = false self.coordinator.sneakPeek.show = false self.edgeAutoOpenActive = false // Set the current view to shelf if it contains files and the user enables openShelfByDefault // Otherwise, if the user has not enabled openLastShelfByDefault, set the view to home if !ShelfStateViewModel.shared.isEmpty && Defaults[.openShelfByDefault] { coordinator.currentView = .shelf } else if !coordinator.openLastTabByDefault { coordinator.currentView = .home } } func closeHello() { Task { @MainActor in withAnimation(animationLibrary.animation) { coordinator.helloAnimationRunning = false close() } } } } ================================================ FILE: boringNotch/models/CalendarModel.swift ================================================ // // CalendarModel.swift // Calendr // // Created by Paker on 31/12/20. // Original source: https://github.com/pakerwreah/Calendr // import Cocoa struct CalendarModel: Equatable { let id: String let account: String let title: String let color: NSColor let isSubscribed: Bool let isReminder: Bool // true if this is a reminder calendar } ================================================ FILE: boringNotch/models/Constants.swift ================================================ // // Constants.swift // boringNotch // // Created by Richard Kunkli on 2024. 10. 17.. // import SwiftUI import Defaults private let availableDirectories = FileManager .default .urls(for: .documentDirectory, in: .userDomainMask) let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! let bundleIdentifier = Bundle.main.bundleIdentifier! let appVersion = "\(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "") (\(Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? ""))" let temporaryDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first! let spacing: CGFloat = 16 struct CustomVisualizer: Codable, Hashable, Equatable, Defaults.Serializable { let UUID: UUID var name: String var url: URL var speed: CGFloat = 1.0 } enum CalendarSelectionState: Codable, Defaults.Serializable { case all case selected(Set) } enum HideNotchOption: String, Defaults.Serializable { case always case nowPlayingOnly case never } // Define notification names at file scope extension Notification.Name { static let mediaControllerChanged = Notification.Name("mediaControllerChanged") } // Media controller types for selection in settings enum MediaControllerType: String, CaseIterable, Identifiable, Defaults.Serializable { case nowPlaying = "Now Playing" case appleMusic = "Apple Music" case spotify = "Spotify" case youtubeMusic = "YouTube Music" var id: String { self.rawValue } } // Sneak peek styles for selection in settings enum SneakPeekStyle: String, CaseIterable, Identifiable, Defaults.Serializable { case standard = "Default" case inline = "Inline" var id: String { self.rawValue } } // Action to perform when Option (⌥) is held while pressing media keys enum OptionKeyAction: String, CaseIterable, Identifiable, Defaults.Serializable { case openSettings = "Open System Settings" case showHUD = "Show HUD" case none = "No Action" var id: String { self.rawValue } } extension Defaults.Keys { // MARK: General static let menubarIcon = Key("menubarIcon", default: true) static let showOnAllDisplays = Key("showOnAllDisplays", default: false) static let automaticallySwitchDisplay = Key("automaticallySwitchDisplay", default: true) static let releaseName = Key("releaseName", default: "Flying Rabbit 🐇🪽") // MARK: Behavior static let minimumHoverDuration = Key("minimumHoverDuration", default: 0.3) static let enableHaptics = Key("enableHaptics", default: true) static let openNotchOnHover = Key("openNotchOnHover", default: true) static let extendHoverArea = Key("extendHoverArea", default: false) static let notchHeightMode = Key( "notchHeightMode", default: WindowHeightMode.matchRealNotchSize ) static let nonNotchHeightMode = Key( "nonNotchHeightMode", default: WindowHeightMode.matchMenuBar ) static let nonNotchHeight = Key("nonNotchHeight", default: 32) static let notchHeight = Key("notchHeight", default: 32) //static let openLastTabByDefault = Key("openLastTabByDefault", default: false) static let showOnLockScreen = Key("showOnLockScreen", default: false) static let hideFromScreenRecording = Key("hideFromScreenRecording", default: false) // MARK: Appearance static let showEmojis = Key("showEmojis", default: false) //static let alwaysShowTabs = Key("alwaysShowTabs", default: true) static let showMirror = Key("showMirror", default: false) static let mirrorShape = Key("mirrorShape", default: MirrorShapeEnum.rectangle) static let settingsIconInNotch = Key("settingsIconInNotch", default: true) static let lightingEffect = Key("lightingEffect", default: true) static let enableShadow = Key("enableShadow", default: true) static let cornerRadiusScaling = Key("cornerRadiusScaling", default: true) static let showNotHumanFace = Key("showNotHumanFace", default: false) static let tileShowLabels = Key("tileShowLabels", default: false) static let showCalendar = Key("showCalendar", default: false) static let hideCompletedReminders = Key("hideCompletedReminders", default: true) static let sliderColor = Key( "sliderUseAlbumArtColor", default: SliderColorEnum.white ) static let playerColorTinting = Key("playerColorTinting", default: true) static let useMusicVisualizer = Key("useMusicVisualizer", default: true) static let customVisualizers = Key<[CustomVisualizer]>("customVisualizers", default: []) static let selectedVisualizer = Key("selectedVisualizer", default: nil) // MARK: Gestures static let enableGestures = Key("enableGestures", default: true) static let closeGestureEnabled = Key("closeGestureEnabled", default: true) static let gestureSensitivity = Key("gestureSensitivity", default: 200.0) // MARK: Media playback static let coloredSpectrogram = Key("coloredSpectrogram", default: true) static let enableSneakPeek = Key("enableSneakPeek", default: false) static let sneakPeekStyles = Key("sneakPeekStyles", default: .standard) static let waitInterval = Key("waitInterval", default: 3) static let showShuffleAndRepeat = Key("showShuffleAndRepeat", default: false) static let enableLyrics = Key("enableLyrics", default: false) static let musicControlSlots = Key<[MusicControlButton]>( "musicControlSlots", default: MusicControlButton.defaultLayout ) static let musicControlSlotLimit = Key( "musicControlSlotLimit", default: MusicControlButton.defaultLayout.count ) // MARK: Battery static let showPowerStatusNotifications = Key("showPowerStatusNotifications", default: true) static let showBatteryIndicator = Key("showBatteryIndicator", default: true) static let showBatteryPercentage = Key("showBatteryPercentage", default: true) static let showPowerStatusIcons = Key("showPowerStatusIcons", default: true) // MARK: Downloads static let enableDownloadListener = Key("enableDownloadListener", default: true) static let enableSafariDownloads = Key("enableSafariDownloads", default: true) static let selectedDownloadIndicatorStyle = Key("selectedDownloadIndicatorStyle", default: DownloadIndicatorStyle.progress) static let selectedDownloadIconStyle = Key("selectedDownloadIconStyle", default: DownloadIconStyle.onlyAppIcon) // MARK: HUD static let hudReplacement = Key("hudReplacement", default: false) static let inlineHUD = Key("inlineHUD", default: false) static let enableGradient = Key("enableGradient", default: false) static let systemEventIndicatorShadow = Key("systemEventIndicatorShadow", default: false) static let systemEventIndicatorUseAccent = Key("systemEventIndicatorUseAccent", default: false) static let showOpenNotchHUD = Key("showOpenNotchHUD", default: true) static let showOpenNotchHUDPercentage = Key("showOpenNotchHUDPercentage", default: true) static let showClosedNotchHUDPercentage = Key("showClosedNotchHUDPercentage", default: false) // Option key modifier behaviour for media keys static let optionKeyAction = Key("optionKeyAction", default: OptionKeyAction.openSettings) // MARK: Shelf static let boringShelf = Key("boringShelf", default: true) static let openShelfByDefault = Key("openShelfByDefault", default: true) static let shelfTapToOpen = Key("shelfTapToOpen", default: true) static let quickShareProvider = Key("quickShareProvider", default: QuickShareProvider.defaultProvider.id) static let copyOnDrag = Key("copyOnDrag", default: false) static let autoRemoveShelfItems = Key("autoRemoveShelfItems", default: false) static let expandedDragDetection = Key("expandedDragDetection", default: true) // MARK: Calendar static let calendarSelectionState = Key("calendarSelectionState", default: .all) static let hideAllDayEvents = Key("hideAllDayEvents", default: false) static let showFullEventTitles = Key("showFullEventTitles", default: false) static let autoScrollToNextEvent = Key("autoScrollToNextEvent", default: true) // MARK: Fullscreen Media Detection static let hideNotchOption = Key("hideNotchOption", default: .nowPlayingOnly) // MARK: Media Controller static let mediaController = Key("mediaController", default: defaultMediaController) // MARK: Advanced Settings static let useCustomAccentColor = Key("useCustomAccentColor", default: false) static let customAccentColorData = Key("customAccentColorData", default: nil) // Show or hide the title bar static let hideTitleBar = Key("hideTitleBar", default: true) // Helper to determine the default media controller based on NowPlaying deprecation status static var defaultMediaController: MediaControllerType { if MusicManager.shared.isNowPlayingDeprecated { return .appleMusic } else { return .nowPlaying } } static let didClearLegacyURLCacheV1 = Key("didClearLegacyURLCache_v1", default: false) } ================================================ FILE: boringNotch/models/EventModel.swift ================================================ // // EventModel.swift // Calendr // // Created by Paker on 24/12/20. // Original source: https://github.com/pakerwreah/Calendr // Modified by Alexander on 2025-05-18. // import Foundation struct EventModel: Equatable, Identifiable { let id: String let start: Date let end: Date let title: String let location: String? let notes: String? let url: URL? let isAllDay: Bool let type: EventType let calendar: CalendarModel let participants: [Participant] let timeZone: TimeZone? let hasRecurrenceRules: Bool let priority: Priority? } enum AttendanceStatus: Comparable { case accepted case maybe case pending case declined case unknown private var comparisonValue: Int { switch self { case .accepted: return 1 case .maybe: return 2 case .declined: return 3 case .pending: return 4 case .unknown: return 5 } } static func < (lhs: Self, rhs: Self) -> Bool { return lhs.comparisonValue < rhs.comparisonValue } } enum EventType: Equatable { case event(AttendanceStatus) case birthday case reminder(completed: Bool) } enum EventStatus: Equatable { case upcoming case inProgress case ended } extension EventType { var isEvent: Bool { if case .event = self { return true } else { return false } } var isBirthday: Bool { self ~= .birthday } var isReminder: Bool { if case .reminder = self { return true } else { return false } } } extension EventModel { var eventStatus: EventStatus { if start > Date() { return .upcoming } else if end > Date() { return .inProgress } else { return .ended } } var attendance: AttendanceStatus { if case .event(let attendance) = type { return attendance } else { return .unknown } } var isMeeting: Bool { !participants.isEmpty } func calendarAppURL() -> URL? { guard let id = id.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { return nil } guard !type.isReminder else { return URL(string: "x-apple-reminderkit://remcdreminder/\(id)") } let date: String if hasRecurrenceRules { let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" if !isAllDay { formatter.timeZone = .init(secondsFromGMT: 0) } if let formattedDate = formatter.string(for: start) { date = "/\(formattedDate)" } else { return nil } } else { date = "" } return URL(string: "ical://ekevent\(date)/\(id)?method=show&options=more") } } struct Participant: Hashable { let name: String let status: AttendanceStatus let isOrganizer: Bool let isCurrentUser: Bool } enum Priority { case high case medium case low } ================================================ FILE: boringNotch/models/MusicControlButton.swift ================================================ // // MusicControlButton.swift // boringNotch // // Created by Alexander on 2025-11-16. // import Defaults enum MusicControlButton: String, CaseIterable, Identifiable, Codable, Defaults.Serializable { case shuffle case previous case playPause case next case repeatMode case volume case favorite case goBackward case goForward case none var id: String { rawValue } static let defaultLayout: [MusicControlButton] = [ .none, .previous, .playPause, .next, .none ] static let minSlotCount: Int = 3 static let maxSlotCount: Int = 5 static let pickerOptions: [MusicControlButton] = [ .shuffle, .previous, .playPause, .next, .repeatMode, .favorite, .volume, .goBackward, .goForward ] var label: String { switch self { case .shuffle: return "Shuffle" case .previous: return "Previous" case .playPause: return "Play/Pause" case .next: return "Next" case .repeatMode: return "Repeat" case .volume: return "Volume" case .favorite: return "Favorite" case .goBackward: return "Backward 15s" case .goForward: return "Forward 15s" case .none: return "Empty slot" } } var iconName: String { switch self { case .shuffle: return "shuffle" case .previous: return "backward.fill" case .playPause: return "playpause" case .next: return "forward.fill" case .repeatMode: return "repeat" case .volume: return "speaker.wave.2.fill" case .favorite: return "heart" case .goBackward: return "gobackward.15" case .goForward: return "goforward.15" case .none: return "" } } var prefersLargeScale: Bool { self == .playPause } } ================================================ FILE: boringNotch/models/PlaybackState.swift ================================================ // // PlaybackState.swift // boringNotch // // Created by Alexander on 2025-03-29. // import Foundation enum RepeatMode: Int, Codable { case off = 1 case one = 2 case all = 3 } struct PlaybackState { var bundleIdentifier: String var isPlaying: Bool = false var title: String = "I'm Handsome" var artist: String = "Me" var album: String = "Self Love" var currentTime: Double = 0 var duration: Double = 0 var playbackRate: Double = 1 var isShuffled: Bool = false var repeatMode: RepeatMode = .off var lastUpdated: Date = Date.distantPast var artwork: Data? var volume: Double = 0.5 var isFavorite: Bool = false } extension PlaybackState: Equatable { static func == (lhs: PlaybackState, rhs: PlaybackState) -> Bool { return lhs.bundleIdentifier == rhs.bundleIdentifier && lhs.isPlaying == rhs.isPlaying && lhs.title == rhs.title && lhs.artist == rhs.artist && lhs.album == rhs.album && lhs.currentTime == rhs.currentTime && lhs.duration == rhs.duration && lhs.isShuffled == rhs.isShuffled && lhs.repeatMode == rhs.repeatMode && lhs.artwork == rhs.artwork && lhs.isFavorite == rhs.isFavorite } } ================================================ FILE: boringNotch/models/SharingStateManager.swift ================================================ // // SharingStateManager.swift // boringNotch // // Created by Alexander on 2025-10-10. // import AppKit import Combine import Foundation extension Notification.Name { static let sharingDidFinish = Notification.Name("com.boringNotch.sharingDidFinish") } @MainActor final class SharingStateManager: ObservableObject { static let shared = SharingStateManager() private var activeSessions: Int = 0 { didSet { let newValue = activeSessions > 0 if newValue != preventNotchClose { preventNotchClose = newValue if !newValue { NotificationCenter.default.post(name: .sharingDidFinish, object: nil) } } } } @Published var preventNotchClose: Bool = false private var activeDelegates: [UUID: SharingLifecycleDelegate] = [:] private init() {} func requestCloseIfReady() { if !preventNotchClose { NotificationCenter.default.post(name: .sharingDidFinish, object: nil) } } func beginInteraction() { activeSessions += 1 } func endInteraction() { if activeSessions > 0 { activeSessions -= 1 } } func makeDelegate(onEnd: (() -> Void)? = nil) -> SharingLifecycleDelegate { let id = UUID() let delegate = SharingLifecycleDelegate(id: id, onEnd: { [weak self] in onEnd?() self?.unregisterDelegate(id: id) }, onBegin: { [weak self] in self?.beginInteraction() }, onFinish: { [weak self] in self?.endInteraction() }) activeDelegates[id] = delegate return delegate } private func unregisterDelegate(id: UUID) { activeDelegates.removeValue(forKey: id) } } final class SharingLifecycleDelegate: NSObject, NSSharingServiceDelegate, NSSharingServicePickerDelegate { let id: UUID private let onEnd: () -> Void private let onBegin: () -> Void private let onFinish: () -> Void private var pickerActive = false private var serviceInProgress = false private var finished = false private var timeoutTask: Task? init(id: UUID, onEnd: @escaping () -> Void, onBegin: @escaping () -> Void, onFinish: @escaping () -> Void) { self.id = id self.onEnd = onEnd self.onBegin = onBegin self.onFinish = onFinish } deinit { timeoutTask?.cancel() } func markPickerBegan() { guard !pickerActive else { return } pickerActive = true onBegin() } func markServiceBegan() { guard !serviceInProgress else { return } serviceInProgress = true onBegin() startTimeoutFallback() } private func startTimeoutFallback() { timeoutTask?.cancel() timeoutTask = Task { @MainActor [weak self] in try? await Task.sleep(for: .seconds(2)) guard let self = self, !Task.isCancelled else { return } if !self.finished { self.finishIfNeeded() } } } private func finishIfNeeded() { guard !finished else { return } finished = true timeoutTask?.cancel() onFinish() onEnd() } // MARK: - NSSharingServicePickerDelegate func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, didChoose service: NSSharingService?) { if service == nil { if pickerActive && !serviceInProgress { finishIfNeeded() } return } service?.delegate = self serviceInProgress = true startTimeoutFallback() } // MARK: - NSSharingServiceDelegate func sharingService(_ sharingService: NSSharingService, willShareItems items: [Any]) { if !pickerActive && !serviceInProgress { onBegin() } serviceInProgress = true } func sharingService(_ sharingService: NSSharingService, didShareItems items: [Any]) { finishIfNeeded() } func sharingService(_ sharingService: NSSharingService, didFailToShareItems items: [Any], error: Error) { finishIfNeeded() } } ================================================ FILE: boringNotch/observers/DragDetector.swift ================================================ // // DragDetector.swift // boringNotch // // Created by Alexander on 2025-11-20. // import Cocoa import UniformTypeIdentifiers final class DragDetector { // MARK: - Callbacks typealias VoidCallback = () -> Void typealias PositionCallback = (_ globalPoint: CGPoint) -> Void var onDragEntersNotchRegion: VoidCallback? var onDragExitsNotchRegion: VoidCallback? var onDragMove: PositionCallback? private var mouseDownMonitor: Any? private var mouseDraggedMonitor: Any? private var mouseUpMonitor: Any? private var pasteboardChangeCount: Int = -1 private var isDragging: Bool = false private var isContentDragging: Bool = false private var hasEnteredNotchRegion: Bool = false private let notchRegion: CGRect private let dragPasteboard = NSPasteboard(name: .drag) init(notchRegion: CGRect) { self.notchRegion = notchRegion } // MARK: - Private Helpers /// Checks if the drag pasteboard contains valid content types that can be dropped on the shelf private func hasValidDragContent() -> Bool { let validTypes: [NSPasteboard.PasteboardType] = [ .fileURL, NSPasteboard.PasteboardType(UTType.url.identifier), .string ] return dragPasteboard.types?.contains(where: validTypes.contains) ?? false } func startMonitoring() { stopMonitoring() // Track pasteboard to detect content drag mouseDownMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseDown]) { [weak self] _ in guard let self = self else { return } self.pasteboardChangeCount = self.dragPasteboard.changeCount self.isDragging = true self.isContentDragging = false self.hasEnteredNotchRegion = false } // Track drag movement and notch region intersection mouseDraggedMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseDragged]) { [weak self] event in guard let self = self else { return } guard self.isDragging else { return } let newContent = self.dragPasteboard.changeCount != self.pasteboardChangeCount // Detect if actual content is being dragged AND it's valid content if newContent && !self.isContentDragging && self.hasValidDragContent() { self.isContentDragging = true } // Only process position when content is being dragged if self.isContentDragging { let mouseLocation = NSEvent.mouseLocation self.onDragMove?(mouseLocation) // Track notch region entry/exit let containsMouse = self.notchRegion.contains(mouseLocation) if containsMouse && !self.hasEnteredNotchRegion { self.hasEnteredNotchRegion = true self.onDragEntersNotchRegion?() } else if !containsMouse && self.hasEnteredNotchRegion { self.hasEnteredNotchRegion = false self.onDragExitsNotchRegion?() } } } mouseUpMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseUp]) { [weak self] _ in guard let self = self else { return } guard self.isDragging else { return } self.isDragging = false self.isContentDragging = false self.hasEnteredNotchRegion = false self.pasteboardChangeCount = -1 } } func stopMonitoring() { [mouseDownMonitor, mouseDraggedMonitor, mouseUpMonitor].forEach { monitor in if let monitor = monitor { NSEvent.removeMonitor(monitor) } } mouseDownMonitor = nil mouseDraggedMonitor = nil mouseUpMonitor = nil isDragging = false isContentDragging = false hasEnteredNotchRegion = false } deinit { stopMonitoring() } } ================================================ FILE: boringNotch/observers/FullscreenMediaDetection.swift ================================================ // // FullscreenMediaDetection.swift // boringNotch // // Created by Richard Kunkli on 06/09/2024. // import Foundation import Combine import Defaults import MacroVisionKit @MainActor final class FullscreenMediaDetector: ObservableObject { static let shared = FullscreenMediaDetector() @Published var fullscreenStatus: [String: Bool] = [:] private var monitorTask: Task? private init() { startMonitoring() } deinit { monitorTask?.cancel() } private func startMonitoring() { monitorTask = Task { @MainActor in let stream = await FullScreenMonitor.shared.spaceChanges() for await spaces in stream { updateStatus(with: spaces) } } } private func updateStatus(with spaces: [MacroVisionKit.FullScreenMonitor.SpaceInfo]) { var newStatus: [String: Bool] = [:] for space in spaces { if let uuid = space.screenUUID { let shouldDetect: Bool if Defaults[.hideNotchOption] == .nowPlayingOnly, let musicSourceBundle = MusicManager.shared.bundleIdentifier { shouldDetect = space.runningApps.contains(musicSourceBundle) } else { shouldDetect = true } newStatus[uuid] = shouldDetect } } self.fullscreenStatus = newStatus } } ================================================ FILE: boringNotch/observers/MediaKeyInterceptor.swift ================================================ // // MediaKeyInterceptor.swift // boringNotch // // Created by Alexander on 2025-11-23. import Foundation import AppKit import ApplicationServices import Defaults import AVFoundation private let kSystemDefinedEventType = CGEventType(rawValue: 14)! final class MediaKeyInterceptor { static let shared = MediaKeyInterceptor() private enum NXKeyType: Int { case soundUp = 0 case soundDown = 1 case brightnessUp = 2 case brightnessDown = 3 case mute = 7 case keyboardBrightnessUp = 21 case keyboardBrightnessDown = 22 } private var eventTap: CFMachPort? private var runLoopSource: CFRunLoopSource? private let step: Float = 1.0 / 16.0 private var audioPlayer: AVAudioPlayer? private init() {} // MARK: - Accessibility (via XPC) func requestAccessibilityAuthorization() { XPCHelperClient.shared.requestAccessibilityAuthorization() } func ensureAccessibilityAuthorization(promptIfNeeded: Bool = false) async -> Bool { await XPCHelperClient.shared.ensureAccessibilityAuthorization(promptIfNeeded: promptIfNeeded) } // MARK: - Event Tap func start(promptIfNeeded: Bool = false) async { guard eventTap == nil else { return } // Ensure HUD replacement is enabled guard Defaults[.hudReplacement] else { stop() return } // Check accessibility authorization let authorized = await XPCHelperClient.shared.isAccessibilityAuthorized() if !authorized { if promptIfNeeded { let granted = await ensureAccessibilityAuthorization(promptIfNeeded: true) guard granted else { return } } else { return } } let mask = CGEventMask(1 << kSystemDefinedEventType.rawValue) eventTap = CGEvent.tapCreate( tap: .cghidEventTap, place: .headInsertEventTap, options: .defaultTap, eventsOfInterest: mask, callback: { _, _, cgEvent, userInfo in guard let userInfo else { return Unmanaged.passRetained(cgEvent) } let interceptor = Unmanaged.fromOpaque(userInfo).takeUnretainedValue() return interceptor.handleEvent(cgEvent) }, userInfo: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()) ) if let eventTap { runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0) if let runLoopSource { CFRunLoopAddSource(CFRunLoopGetMain(), runLoopSource, .commonModes) } CGEvent.tapEnable(tap: eventTap, enable: true) } } func stop() { if let eventTap { CGEvent.tapEnable(tap: eventTap, enable: false) } if let runLoopSource { CFRunLoopRemoveSource(CFRunLoopGetMain(), runLoopSource, .commonModes) } runLoopSource = nil eventTap = nil } // MARK: - Event Handling private func handleEvent(_ cgEvent: CGEvent) -> Unmanaged? { // Ensure the CGEvent has a valid type before converting to NSEvent guard cgEvent.type != .null else { return Unmanaged.passRetained(cgEvent) } guard let nsEvent = NSEvent(cgEvent: cgEvent), nsEvent.type == .systemDefined, nsEvent.subtype.rawValue == 8 else { return Unmanaged.passRetained(cgEvent) } let data1 = nsEvent.data1 let keyCode = (data1 & 0xFFFF_0000) >> 16 let stateByte = ((data1 & 0xFF00) >> 8) // 0xA = key down, 0xB = key up. Only handle key down. guard stateByte == 0xA, let keyType = NXKeyType(rawValue: keyCode) else { return Unmanaged.passRetained(cgEvent) } let flags = nsEvent.modifierFlags let option = flags.contains(.option) let shift = flags.contains(.shift) let command = flags.contains(.command) // Handle option key action (without shift) if option && !shift { if handleOptionAction(for: keyType, command: command) { return nil } } // Handle normal key press handleKeyPress(keyType: keyType, option: option, shift: shift, command: command) return nil } private func handleOptionAction(for keyType: NXKeyType, command: Bool) -> Bool { let action = Defaults[.optionKeyAction] switch action { case .openSettings: openSystemSettings(for: keyType, command: command) return true case .showHUD: showHUD(for: keyType, command: command) return true case .none: return true } } private func prepareAudioPlayerIfNeeded() { guard audioPlayer == nil else { return } let defaultPath = "/System/Library/LoginPlugins/BezelServices.loginPlugin/Contents/Resources/volume.aiff" if FileManager.default.fileExists(atPath: defaultPath) { do { audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: defaultPath)) print("🔊 [MediaKeyInterceptor] Loaded default Bezel audio from: \(defaultPath)") } catch { print("⚠️ [MediaKeyInterceptor] Failed to init AVAudioPlayer with default path \(defaultPath): \(error.localizedDescription)") } } else { print("⚠️ [MediaKeyInterceptor] Default bezel audio not found at: \(defaultPath)") } if let player = audioPlayer { player.volume = 1.0 player.numberOfLoops = 0 player.prepareToPlay() } } private func playFeedbackSound() { guard let feedback = UserDefaults.standard.persistentDomain(forName: "NSGlobalDomain")?["com.apple.sound.beep.feedback"] as? Int, feedback == 1 else { return } prepareAudioPlayerIfNeeded() guard let player = audioPlayer else { print("⚠️ [MediaKeyInterceptor] No audio player available to play feedback sound") return } if let url = player.url { print("🔊 [MediaKeyInterceptor] Playing feedback sound from: \(url.path)") } else { print("🔊 [MediaKeyInterceptor] Playing feedback sound (no url available for AVAudioPlayer)") } if player.isPlaying { player.stop() player.currentTime = 0 } player.play() } private func handleKeyPress(keyType: NXKeyType, option: Bool, shift: Bool, command: Bool) { let stepDivisor: Float = (option && shift) ? 4.0 : 1.0 switch keyType { case .soundUp: Task { @MainActor in self.playFeedbackSound() VolumeManager.shared.increase(stepDivisor: stepDivisor) } case .soundDown: Task { @MainActor in self.playFeedbackSound() VolumeManager.shared.decrease(stepDivisor: stepDivisor) } case .mute: Task { @MainActor in VolumeManager.shared.toggleMuteAction() } case .brightnessUp, .keyboardBrightnessUp: let delta = step / stepDivisor adjustBrightness(delta: delta, keyboard: keyType == .keyboardBrightnessUp || command) case .brightnessDown, .keyboardBrightnessDown: let delta = -(step / stepDivisor) adjustBrightness(delta: delta, keyboard: keyType == .keyboardBrightnessDown || command) } } private func adjustBrightness(delta: Float, keyboard: Bool) { Task { @MainActor in if keyboard { KeyboardBacklightManager.shared.setRelative(delta: delta) } else { BrightnessManager.shared.setRelative(delta: delta) } } } private func showHUD(for keyType: NXKeyType, command: Bool) { Task { @MainActor in switch keyType { case .soundUp, .soundDown, .mute: let v = VolumeManager.shared.rawVolume BoringViewCoordinator.shared.toggleSneakPeek(status: true, type: .volume, value: CGFloat(v)) case .brightnessUp, .brightnessDown: if command { let v = KeyboardBacklightManager.shared.rawBrightness BoringViewCoordinator.shared.toggleSneakPeek(status: true, type: .backlight, value: CGFloat(v)) } else { let v = BrightnessManager.shared.rawBrightness BoringViewCoordinator.shared.toggleSneakPeek(status: true, type: .brightness, value: CGFloat(v)) } case .keyboardBrightnessUp, .keyboardBrightnessDown: let v = KeyboardBacklightManager.shared.rawBrightness BoringViewCoordinator.shared.toggleSneakPeek(status: true, type: .backlight, value: CGFloat(v)) } } } private func openSystemSettings(for keyType: NXKeyType, command: Bool) { let urlString: String switch keyType { case .soundUp, .soundDown, .mute: urlString = "x-apple.systempreferences:com.apple.preference.sound" case .brightnessUp, .brightnessDown: if command { urlString = "x-apple.systempreferences:com.apple.preference.keyboard" } else { urlString = "x-apple.systempreferences:com.apple.preference.displays" } case .keyboardBrightnessUp, .keyboardBrightnessDown: urlString = "x-apple.systempreferences:com.apple.preference.keyboard" } guard let url = URL(string: urlString) else { return } NSWorkspace.shared.open(url) } } ================================================ FILE: boringNotch/private/CGSSpace.swift ================================================ // // CGSSpace.swift // boringNotch // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. // // Original source: https://github.com/avaidyam/Parrot/ // Modified by Alexander on 2024-10-27 import AppKit /// Small Spaces API wrapper. public final class CGSSpace { private let identifier: CGSSpaceID public var windows: Set = [] { didSet { let remove = oldValue.subtracting(self.windows) let add = self.windows.subtracting(oldValue) CGSRemoveWindowsFromSpaces(_CGSDefaultConnection(), remove.map { $0.windowNumber } as NSArray, [self.identifier]) CGSAddWindowsToSpaces(_CGSDefaultConnection(), add.map { $0.windowNumber } as NSArray, [self.identifier]) } } /// Initialized `CGSSpace`s *MUST* be de-initialized upon app exit! public init(level: Int = 0) { let flag = 0x1 // this value MUST be 1, otherwise, Finder decides to draw desktop icons self.identifier = CGSSpaceCreate(_CGSDefaultConnection(), flag, nil) CGSSpaceSetAbsoluteLevel(_CGSDefaultConnection(), self.identifier, level) CGSShowSpaces(_CGSDefaultConnection(), [self.identifier]) } deinit { CGSHideSpaces(_CGSDefaultConnection(), [self.identifier]) CGSSpaceDestroy(_CGSDefaultConnection(), self.identifier) } } // CGSSpace stuff: fileprivate typealias CGSConnectionID = UInt fileprivate typealias CGSSpaceID = UInt64 @_silgen_name("_CGSDefaultConnection") fileprivate func _CGSDefaultConnection() -> CGSConnectionID @_silgen_name("CGSSpaceCreate") fileprivate func CGSSpaceCreate(_ cid: CGSConnectionID, _ unknown: Int, _ options: NSDictionary?) -> CGSSpaceID @_silgen_name("CGSSpaceDestroy") fileprivate func CGSSpaceDestroy(_ cid: CGSConnectionID, _ space: CGSSpaceID) @_silgen_name("CGSSpaceSetAbsoluteLevel") fileprivate func CGSSpaceSetAbsoluteLevel(_ cid: CGSConnectionID, _ space: CGSSpaceID, _ level: Int) @_silgen_name("CGSAddWindowsToSpaces") fileprivate func CGSAddWindowsToSpaces(_ cid: CGSConnectionID, _ windows: NSArray, _ spaces: NSArray) @_silgen_name("CGSRemoveWindowsFromSpaces") fileprivate func CGSRemoveWindowsFromSpaces(_ cid: CGSConnectionID, _ windows: NSArray, _ spaces: NSArray) @_silgen_name("CGSHideSpaces") fileprivate func CGSHideSpaces(_ cid: CGSConnectionID, _ spaces: NSArray) @_silgen_name("CGSShowSpaces") fileprivate func CGSShowSpaces(_ cid: CGSConnectionID, _ spaces: NSArray) ================================================ FILE: boringNotch/sizing/matters.swift ================================================ // // sizeMatters.swift // boringNotch // // Created by Harsh Vardhan Goswami on 05/08/24. // import Defaults import Foundation import SwiftUI let downloadSneakSize: CGSize = .init(width: 65, height: 1) let batterySneakSize: CGSize = .init(width: 160, height: 1) let shadowPadding: CGFloat = 20 let openNotchSize: CGSize = .init(width: 640, height: 190) let windowSize: CGSize = .init(width: openNotchSize.width, height: openNotchSize.height + shadowPadding) let cornerRadiusInsets: (opened: (top: CGFloat, bottom: CGFloat), closed: (top: CGFloat, bottom: CGFloat)) = (opened: (top: 19, bottom: 24), closed: (top: 6, bottom: 14)) enum MusicPlayerImageSizes { static let cornerRadiusInset: (opened: CGFloat, closed: CGFloat) = (opened: 13.0, closed: 4.0) static let size = (opened: CGSize(width: 90, height: 90), closed: CGSize(width: 20, height: 20)) } @MainActor func getScreenFrame(_ screenUUID: String? = nil) -> CGRect? { var selectedScreen = NSScreen.main if let uuid = screenUUID { selectedScreen = NSScreen.screen(withUUID: uuid) } if let screen = selectedScreen { return screen.frame } return nil } @MainActor func getClosedNotchSize(screenUUID: String? = nil) -> CGSize { // Default notch size, to avoid using optionals var notchHeight: CGFloat = Defaults[.nonNotchHeight] var notchWidth: CGFloat = 185 var selectedScreen = NSScreen.main if let uuid = screenUUID { selectedScreen = NSScreen.screen(withUUID: uuid) } // Check if the screen is available if let screen = selectedScreen { // Calculate and set the exact width of the notch if let topLeftNotchpadding: CGFloat = screen.auxiliaryTopLeftArea?.width, let topRightNotchpadding: CGFloat = screen.auxiliaryTopRightArea?.width { notchWidth = screen.frame.width - topLeftNotchpadding - topRightNotchpadding + 4 } // Check if the Mac has a notch if screen.safeAreaInsets.top > 0 { // This is a display WITH a notch - use notch height settings notchHeight = Defaults[.notchHeight] if Defaults[.notchHeightMode] == .matchRealNotchSize { notchHeight = screen.safeAreaInsets.top } else if Defaults[.notchHeightMode] == .matchMenuBar { notchHeight = screen.frame.maxY - screen.visibleFrame.maxY } } else { // This is a display WITHOUT a notch - use non-notch height settings notchHeight = Defaults[.nonNotchHeight] if Defaults[.nonNotchHeightMode] == .matchMenuBar { notchHeight = screen.frame.maxY - screen.visibleFrame.maxY } } } return .init(width: notchWidth, height: notchHeight) } ================================================ FILE: boringNotch/utils/Logger.swift ================================================ import Foundation import SwiftUI enum LogCategory: String { case lifecycle = "🔄" case memory = "💾" case performance = "⚡️" case ui = "🎨" case network = "🌐" case error = "❌" case warning = "⚠️" case success = "✅" case debug = "🔍" } struct Logger { static func log( _ message: String, category: LogCategory, file: String = #file, function: String = #function, line: Int = #line ) { let fileName = (file as NSString).lastPathComponent let timestamp = ISO8601DateFormatter().string(from: Date()) print("\(category.rawValue) [\(timestamp)] [\(fileName):\(line)] \(function) - \(message)") } static func trackMemory( file: String = #file, function: String = #function, line: Int = #line ) { var info = mach_task_basic_info() var count = mach_msg_type_number_t(MemoryLayout.size)/4 let kerr: kern_return_t = withUnsafeMutablePointer(to: &info) { $0.withMemoryRebound(to: integer_t.self, capacity: 1) { task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count) } } if kerr == KERN_SUCCESS { let usedMB = Double(info.resident_size) / 1024.0 / 1024.0 log(String(format: "Memory used: %.2f MB", usedMB), category: .memory, file: file, function: function, line: line) } } } extension View { func trackLifecycle(_ identifier: String) -> some View { self.modifier(ViewLifecycleTracker(identifier: identifier)) } } struct ViewLifecycleTracker: ViewModifier { let identifier: String func body(content: Content) -> some View { content .onAppear { Logger.log("\(identifier) appeared", category: .lifecycle) Logger.trackMemory() } .onDisappear { Logger.log("\(identifier) disappeared", category: .lifecycle) Logger.trackMemory() } } } ================================================ FILE: boringNotch.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 70; objects = { /* Begin PBXBuildFile section */ 1100290C2E847E2800035A57 /* NSItemProvider+LoadHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1100290B2E847E2800035A57 /* NSItemProvider+LoadHelpers.swift */; }; 110029272E84FD4C00035A57 /* TemporaryFileStorageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 110029262E84FD4C00035A57 /* TemporaryFileStorageService.swift */; }; 1100292A2E8691B400035A57 /* FileShareView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 110029292E8691B400035A57 /* FileShareView.swift */; }; 1100292E2E86940F00035A57 /* QuickShareService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1100292D2E86940F00035A57 /* QuickShareService.swift */; }; 1113ABC52E80E27000EC13B2 /* ShelfItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1113ABC32E80E27000EC13B2 /* ShelfItemView.swift */; }; 1113ABC62E80E27000EC13B2 /* ShelfPersistenceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1113ABBE2E80E27000EC13B2 /* ShelfPersistenceService.swift */; }; 1113ABC82E80E27000EC13B2 /* ShelfItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1113ABB72E80E27000EC13B2 /* ShelfItem.swift */; }; 1113ABCA2E80E27000EC13B2 /* ShelfSelectionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1113ABC02E80E27000EC13B2 /* ShelfSelectionModel.swift */; }; 1113ABCB2E80E27000EC13B2 /* QuickLookService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1113ABBA2E80E27000EC13B2 /* QuickLookService.swift */; }; 1113ABCC2E80E27000EC13B2 /* ShelfDropService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1113ABBD2E80E27000EC13B2 /* ShelfDropService.swift */; }; 1113ABCE2E80E27000EC13B2 /* ShelfActionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1113ABBC2E80E27000EC13B2 /* ShelfActionService.swift */; }; 1113ABD02E80E6BB00EC13B2 /* ThumbnailService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1113ABCF2E80E6BB00EC13B2 /* ThumbnailService.swift */; }; 111BE95D2ECD71E10079DD4E /* AsyncXPCConnection in Frameworks */ = {isa = PBXBuildFile; productRef = 111BE95C2ECD71E10079DD4E /* AsyncXPCConnection */; }; 111BE9952ECF2DF40079DD4E /* DragDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 111BE9942ECF2DEF0079DD4E /* DragDetector.swift */; }; 111BEA512ECFBF7F0079DD4E /* MacroVisionKit in Frameworks */ = {isa = PBXBuildFile; productRef = 111BEA502ECFBF7F0079DD4E /* MacroVisionKit */; }; 111BEA5F2ED07A340079DD4E /* MacroVisionKit in Frameworks */ = {isa = PBXBuildFile; productRef = 111BEA5E2ED07A340079DD4E /* MacroVisionKit */; }; 111BEA612ED09B1B0079DD4E /* NSScreen+UUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 111BEA602ED09B1B0079DD4E /* NSScreen+UUID.swift */; }; 111BEA6F2ED166E20079DD4E /* MacroVisionKit in Frameworks */ = {isa = PBXBuildFile; productRef = 111BEA6E2ED166E20079DD4E /* MacroVisionKit */; }; 112B0EB82E30DD0F00562D6C /* MediaRemoteAdapterTestClient in Resources */ = {isa = PBXBuildFile; fileRef = 112B0EB52E30DD0F00562D6C /* MediaRemoteAdapterTestClient */; }; 112B0EB92E30DD0F00562D6C /* mediaremote-adapter.pl in Resources */ = {isa = PBXBuildFile; fileRef = 112B0EB32E30DD0F00562D6C /* mediaremote-adapter.pl */; }; 112B0EBB2E30DD5000562D6C /* MediaRemoteAdapter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 112B0EBA2E30DD5000562D6C /* MediaRemoteAdapter.framework */; }; 112FB7352CCF16F70015238C /* NotchSpaceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 112FB7342CCF16F70015238C /* NotchSpaceManager.swift */; }; 1132E5122E777B6E0068732D /* YouTubeMusicModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1132E5102E777B6E0068732D /* YouTubeMusicModels.swift */; }; 1132E5142E777B920068732D /* YouTubeMusicNetworking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1132E5132E777B920068732D /* YouTubeMusicNetworking.swift */; }; 1132E5162E777C140068732D /* YouTubeMusicAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1132E5152E777C140068732D /* YouTubeMusicAuthentication.swift */; }; 1153BD8F2D986B1F00979FB0 /* MediaControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1153BD8D2D986B1F00979FB0 /* MediaControllerProtocol.swift */; }; 1153BD912D986DB300979FB0 /* PlaybackState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1153BD902D986DB300979FB0 /* PlaybackState.swift */; }; 1153BD932D986E4300979FB0 /* AppleMusicController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1153BD922D986E4300979FB0 /* AppleMusicController.swift */; }; 1153BD982D9881F900979FB0 /* AppleScriptHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1153BD972D9881F900979FB0 /* AppleScriptHelper.swift */; }; 1153BD9A2D98824300979FB0 /* SpotifyController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1153BD992D98824300979FB0 /* SpotifyController.swift */; }; 1153BD9C2D98853B00979FB0 /* NowPlayingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1153BD9B2D98853B00979FB0 /* NowPlayingController.swift */; }; 1153BDA72D99B22200979FB0 /* YouTubeMusicController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1153BDA62D99B22200979FB0 /* YouTubeMusicController.swift */; }; 115C12EC2ED3D003009754CA /* OpenNotchHUD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 115C12EB2ED3D003009754CA /* OpenNotchHUD.swift */; }; 1160F8D82DD98230006FBB94 /* NotchShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1160F8D72DD98230006FBB94 /* NotchShape.swift */; }; 1163988D2DF5CAB40052E6AF /* EventModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1163988C2DF5CAB40052E6AF /* EventModel.swift */; }; 1163988F2DF5CC870052E6AF /* CalendarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1163988E2DF5CC870052E6AF /* CalendarModel.swift */; }; 116398962DF5D6C00052E6AF /* CalendarServiceProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 116398952DF5D6C00052E6AF /* CalendarServiceProviding.swift */; }; 117AB5172E30E09C00558921 /* MediaRemoteAdapter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 112B0EBA2E30DD5000562D6C /* MediaRemoteAdapter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 118D1FD12E98FF5F00A2FF63 /* SharingStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 118D1FD02E98FF5F00A2FF63 /* SharingStateManager.swift */; }; 118EBE252E92DCCB00D54B5A /* AssociatedObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 118EBE242E92DCCB00D54B5A /* AssociatedObject.swift */; }; 118EBE272E92DE8400D54B5A /* NSMenu+AssociatedObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 118EBE262E92DE7400D54B5A /* NSMenu+AssociatedObject.swift */; }; 118EBE292E946B3F00D54B5A /* ShareServiceFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 118EBE282E946B3F00D54B5A /* ShareServiceFinder.swift */; }; 118EBE2D2E97165600D54B5A /* Bookmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 118EBE2C2E97165600D54B5A /* Bookmark.swift */; }; 118EBE312E9717DB00D54B5A /* URL+SecurityScoped.swift in Sources */ = {isa = PBXBuildFile; fileRef = 118EBE302E9717DB00D54B5A /* URL+SecurityScoped.swift */; }; 118EBE3B2E9720C500D54B5A /* ShelfStateViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 118EBE3A2E9720C500D54B5A /* ShelfStateViewModel.swift */; }; 1194E87C2EA19E09009C82D6 /* ImageProcessingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1194E87B2EA19E09009C82D6 /* ImageProcessingService.swift */; }; 1194E8852EA57D23009C82D6 /* SkyLightWindow in Frameworks */ = {isa = PBXBuildFile; productRef = 1194E8842EA57D23009C82D6 /* SkyLightWindow */; }; 1194E8872EA6DDA7009C82D6 /* BoringNotchSkyLightWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1194E8862EA6DDA7009C82D6 /* BoringNotchSkyLightWindow.swift */; }; 1194E9402EACC652009C82D6 /* Color+AccentColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1194E93F2EACC652009C82D6 /* Color+AccentColor.swift */; }; 11A45C792E34E63100CEB175 /* MediaChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11A45C782E34E63100CEB175 /* MediaChecker.swift */; }; 11C5E3132DFE85970065821E /* SettingsWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11C5E3112DFE85970065821E /* SettingsWindowController.swift */; }; 11C5E3162DFE88510065821E /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11C5E3152DFE88510065821E /* SettingsView.swift */; }; 11CC44A22CEE614100C7244B /* BoringViewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11CC44A12CEE614100C7244B /* BoringViewCoordinator.swift */; }; 11CFC65B2E097E9D00748C80 /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11CFC65A2E097E9D00748C80 /* WelcomeView.swift */; }; 11CFC65F2E097F2F00748C80 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11CFC65E2E097F2100748C80 /* OnboardingView.swift */; }; 11CFC6612E097F6800748C80 /* PermissionsRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11CFC6602E097F6800748C80 /* PermissionsRequestView.swift */; }; 11CFC6632E09918400748C80 /* MusicControllerSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11CFC6622E09917B00748C80 /* MusicControllerSelectionView.swift */; }; 11CFC6652E09C7B300748C80 /* OnboardingFinishView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11CFC6642E09C7B300748C80 /* OnboardingFinishView.swift */; }; 11D58EA22E760AE100FA8377 /* ImageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11D58EA12E760AE100FA8377 /* ImageService.swift */; }; 11EFCD702E8E92D600D0B974 /* ShelfItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11EFCD6F2E8E92D600D0B974 /* ShelfItemViewModel.swift */; }; 11F747CE2EC75CEA00F841DB /* DragPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11F747CD2EC75CEA00F841DB /* DragPreviewView.swift */; }; 11F7485B2EC9AABA00F841DB /* BoringNotchXPCHelper.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 11F7484F2EC9AABA00F841DB /* BoringNotchXPCHelper.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 11F748682EC9AC9600F841DB /* BoringNotchXPCHelperProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11F748652EC9AC9600F841DB /* BoringNotchXPCHelperProtocol.swift */; }; 11F748692EC9AC9600F841DB /* XPCHelperClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11F748662EC9AC9600F841DB /* XPCHelperClient.swift */; }; 11F748732EC9DA9300F841DB /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 11F748722EC9DA9300F841DB /* Lottie */; }; 11F748822ECB07A400F841DB /* MusicControlButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11F748812ECB07A400F841DB /* MusicControlButton.swift */; }; 11F748842ECB27DC00F841DB /* MusicSlotConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11F748832ECB27DC00F841DB /* MusicSlotConfigurationView.swift */; }; 14288DDC2C6E015000B9F80C /* AudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14288DD62C6E015000B9F80C /* AudioPlayer.swift */; }; 14288DE82C6E01C800B9F80C /* ProgressIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14288DE72C6E01C800B9F80C /* ProgressIndicator.swift */; }; 14288E0C2C6F8EC000B9F80C /* AppIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14288E0B2C6F8EC000B9F80C /* AppIcons.swift */; }; 1443E7F32C609DCE0027C1FC /* matters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1443E7F22C609DCE0027C1FC /* matters.swift */; }; 147163982C5D35B70068B555 /* MusicVisualizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 147163972C5D35B70068B555 /* MusicVisualizer.swift */; }; 1471639A2C5D35FF0068B555 /* MusicManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 147163992C5D35FF0068B555 /* MusicManager.swift */; }; 1471A8592C6281BD0058408D /* BoringNotchWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1471A8582C6281BD0058408D /* BoringNotchWindow.swift */; }; 149E0B972C737D00006418B1 /* WebcamManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149E0B962C737D00006418B1 /* WebcamManager.swift */; }; 149E0B9A2C737D40006418B1 /* WebcamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149E0B992C737D40006418B1 /* WebcamView.swift */; }; 14A7E5882C64A89C008C1BE9 /* HelloAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14A7E5872C64A89C008C1BE9 /* HelloAnimation.swift */; }; 14A7E5972C65FD31008C1BE9 /* boring.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 14A7E5962C65FD31008C1BE9 /* boring.m4a */; }; 14C08BB62C8DE42D000F8AA0 /* CalendarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C08BB52C8DE42D000F8AA0 /* CalendarManager.swift */; }; 14C08BB92C8DE4B1000F8AA0 /* BoringCalendar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C08BB82C8DE4B1000F8AA0 /* BoringCalendar.swift */; }; 14C08BC12C8E03AD000F8AA0 /* NSImage+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C08BC02C8E03AD000F8AA0 /* NSImage+Extensions.swift */; }; 14CEF4162C5CAED300855D72 /* boringNotchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14CEF4152C5CAED300855D72 /* boringNotchApp.swift */; }; 14CEF4182C5CAED300855D72 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14CEF4172C5CAED300855D72 /* ContentView.swift */; }; 14CEF41A2C5CAED400855D72 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 14CEF4192C5CAED400855D72 /* Assets.xcassets */; }; 14CEF41D2C5CAED400855D72 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 14CEF41C2C5CAED400855D72 /* Preview Assets.xcassets */; }; 14D0321A2C68F32E0096E6A1 /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = 14D032192C68F32E0096E6A1 /* LaunchAtLogin */; }; 14D0321D2C68F3350096E6A1 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 14D0321C2C68F3350096E6A1 /* Sparkle */; }; 14D570B92C5E98A20011E668 /* drop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D570B82C5E98A20011E668 /* drop.swift */; }; 14D570BC2C5E98EB0011E668 /* generic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D570BB2C5E98EB0011E668 /* generic.swift */; }; 14D570C02C5EA5870011E668 /* AnimatedFace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D570BF2C5EA5870011E668 /* AnimatedFace.swift */; }; 14D570C22C5EAFBF0011E668 /* EmptyState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D570C12C5EAFBF0011E668 /* EmptyState.swift */; }; 14D570C62C5F38210011E668 /* BoringHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D570C52C5F38210011E668 /* BoringHeader.swift */; }; 14D570C92C5F38890011E668 /* BoringViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D570C82C5F38890011E668 /* BoringViewModel.swift */; }; 14D570CB2C5F4B2C0011E668 /* BatteryStatusViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D570CA2C5F4B2C0011E668 /* BatteryStatusViewModel.swift */; }; 14D570CD2C5F4BB70011E668 /* BoringBattery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D570CC2C5F4BB70011E668 /* BoringBattery.swift */; }; 14D570D22C5F6C6A0011E668 /* BoringExtrasMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D570D12C5F6C6A0011E668 /* BoringExtrasMenu.swift */; }; 14E9FEAA2C70BF610062E83F /* DownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E9FEA92C70BF610062E83F /* DownloadView.swift */; }; 14E9FEAE2C7325770062E83F /* Button+Bouncing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E9FEAD2C7325770062E83F /* Button+Bouncing.swift */; }; 14FC6E502C7DED5600C7BEA5 /* DataTypes+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14FC6E4F2C7DED5600C7BEA5 /* DataTypes+Extensions.swift */; }; 507266DB2C908E2E00A2D00D /* HoverButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507266DA2C908E2E00A2D00D /* HoverButton.swift */; }; 5917FD112E57891600E87F1C /* MediaKeyInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5917FD102E57891600E87F1C /* MediaKeyInterceptor.swift */; }; 5955950D2E900ED800C66711 /* ApplicationRelauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5955950C2E900ED800C66711 /* ApplicationRelauncher.swift */; }; 59D8C23C2E589FAA00147B33 /* VolumeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59D8C23B2E589FAA00147B33 /* VolumeManager.swift */; }; 9A0887322C7A693000C160EA /* TabButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A0887312C7A693000C160EA /* TabButton.swift */; }; 9A0887352C7AFF8E00C160EA /* TabSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A0887342C7AFF8E00C160EA /* TabSelectionView.swift */; }; 9A987A0D2C73CA66005CA465 /* ShelfView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A987A032C73CA66005CA465 /* ShelfView.swift */; }; 9A987A102C73CA8D005CA465 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = 9A987A0F2C73CA8D005CA465 /* Collections */; }; 9AB0C6BD2C73C9CB00F7CD30 /* NotchHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AB0C6BB2C73C9CB00F7CD30 /* NotchHomeView.swift */; }; B10348D92C74E56000475897 /* ConditionalModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10348D82C74E56000475897 /* ConditionalModifier.swift */; }; B10F84A32C6C9596009F3026 /* TestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10F84A22C6C9596009F3026 /* TestView.swift */; }; B141C2412CA5F53F00AC8CC8 /* SparkleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B141C2402CA5F53E00AC8CC8 /* SparkleView.swift */; }; B1628B922CC260C0003D8DF3 /* SwiftUIIntrospect in Frameworks */ = {isa = PBXBuildFile; productRef = B1628B912CC260C0003D8DF3 /* SwiftUIIntrospect */; }; B17266DF2C64DFA00031BA0D /* BundleInfos.swift in Sources */ = {isa = PBXBuildFile; fileRef = B17266DE2C64DFA00031BA0D /* BundleInfos.swift */; }; B17266E12C6532560031BA0D /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B17266E02C6532560031BA0D /* Localizable.xcstrings */; }; B17266E32C65F7FB0031BA0D /* WhatsNewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B17266E22C65F7FB0031BA0D /* WhatsNewView.swift */; }; B172AAC02C95DA0B001623F1 /* InlineHUD.swift in Sources */ = {isa = PBXBuildFile; fileRef = B172AABF2C95DA0B001623F1 /* InlineHUD.swift */; }; B18654392C6F4990000B926A /* KeyboardShortcuts in Frameworks */ = {isa = PBXBuildFile; productRef = B18654382C6F4990000B926A /* KeyboardShortcuts */; }; B186543C2C6F49AE000B926A /* ShortcutConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = B186543B2C6F49AE000B926A /* ShortcutConstants.swift */; }; B19016222CC15B3D00E3F12E /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = B19016212CC15B3D00E3F12E /* Defaults */; }; B19016242CC15B5000E3F12E /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = B19016232CC15B4D00E3F12E /* Constants.swift */; }; B19424092CD0FF01003E5DC2 /* LottieAnimationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B19424082CD0FEFE003E5DC2 /* LottieAnimationView.swift */; }; B1A78C822C8BA08100BD51B0 /* FullscreenMediaDetection.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1A78C812C8BA08100BD51B0 /* FullscreenMediaDetection.swift */; }; B1B112912C6A572100093D8F /* EditPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1B112902C6A572100093D8F /* EditPanelView.swift */; }; B1B112932C6A577E00093D8F /* MouseTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1B112922C6A577E00093D8F /* MouseTracker.swift */; }; B1C448962C9712C4001F0858 /* ActionBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C448952C9712C4001F0858 /* ActionBar.swift */; }; B1C448982C972CC4001F0858 /* ListItemPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C448972C972CC4001F0858 /* ListItemPopover.swift */; }; B1C4489B2C97376A001F0858 /* TipStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C4489A2C97376A001F0858 /* TipStore.swift */; }; B1C974342C642B6D0000E707 /* MarqueeTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C974332C642B6D0000E707 /* MarqueeTextView.swift */; }; B1CE8CFE2C6F659400DD9871 /* KeyboardShortcutsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1CE8CFD2C6F659400DD9871 /* KeyboardShortcutsHelper.swift */; }; B1D365CE2C6A979C0047BDBC /* LiveActivityModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D365CD2C6A979C0047BDBC /* LiveActivityModifier.swift */; }; B1D365D02C6A9A6C0047BDBC /* SystemEventIndicatorModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D365CF2C6A9A6C0047BDBC /* SystemEventIndicatorModifier.swift */; }; B1D6FD432C6603730015F173 /* SoftwareUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D6FD422C6603730015F173 /* SoftwareUpdater.swift */; }; B1F0A0022E60000100000001 /* BrightnessManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1F0A0012E60000100000001 /* BrightnessManager.swift */; }; B1F747F92EC7E94000F841DB /* LottieView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1F747F82EC7E94000F841DB /* LottieView.swift */; }; B1FEB4992C7686630066EBBC /* PanGesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1FEB4982C7686630066EBBC /* PanGesture.swift */; }; F38DE6482D8243E7008B5C6D /* BatteryActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F38DE6472D8243E2008B5C6D /* BatteryActivityManager.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 11F748592EC9AABA00F841DB /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 14CEF40A2C5CAED200855D72 /* Project object */; proxyType = 1; remoteGlobalIDString = 11F7484E2EC9AABA00F841DB; remoteInfo = BoringNotchXPCHelper; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ 11F748412EC8051E00F841DB /* Embed XPC Services */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = "$(CONTENTS_FOLDER_PATH)/XPCServices"; dstSubfolderSpec = 16; files = ( 11F7485B2EC9AABA00F841DB /* BoringNotchXPCHelper.xpc in Embed XPC Services */, ); name = "Embed XPC Services"; runOnlyForDeploymentPostprocessing = 0; }; B1ECFA062C6FE58A002ACD87 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 12; dstPath = ""; dstSubfolderSpec = 10; files = ( 117AB5172E30E09C00558921 /* MediaRemoteAdapter.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 1100290B2E847E2800035A57 /* NSItemProvider+LoadHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSItemProvider+LoadHelpers.swift"; sourceTree = ""; }; 110029262E84FD4C00035A57 /* TemporaryFileStorageService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryFileStorageService.swift; sourceTree = ""; }; 110029292E8691B400035A57 /* FileShareView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileShareView.swift; sourceTree = ""; }; 1100292D2E86940F00035A57 /* QuickShareService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickShareService.swift; sourceTree = ""; }; 1113ABB72E80E27000EC13B2 /* ShelfItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShelfItem.swift; sourceTree = ""; }; 1113ABBA2E80E27000EC13B2 /* QuickLookService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickLookService.swift; sourceTree = ""; }; 1113ABBC2E80E27000EC13B2 /* ShelfActionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShelfActionService.swift; sourceTree = ""; }; 1113ABBD2E80E27000EC13B2 /* ShelfDropService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShelfDropService.swift; sourceTree = ""; }; 1113ABBE2E80E27000EC13B2 /* ShelfPersistenceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShelfPersistenceService.swift; sourceTree = ""; }; 1113ABC02E80E27000EC13B2 /* ShelfSelectionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShelfSelectionModel.swift; sourceTree = ""; }; 1113ABC32E80E27000EC13B2 /* ShelfItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShelfItemView.swift; sourceTree = ""; }; 1113ABCF2E80E6BB00EC13B2 /* ThumbnailService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbnailService.swift; sourceTree = ""; }; 111BE9942ECF2DEF0079DD4E /* DragDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DragDetector.swift; sourceTree = ""; }; 111BEA602ED09B1B0079DD4E /* NSScreen+UUID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSScreen+UUID.swift"; sourceTree = ""; }; 112B0EB32E30DD0F00562D6C /* mediaremote-adapter.pl */ = {isa = PBXFileReference; lastKnownFileType = text.script.perl; path = "mediaremote-adapter.pl"; sourceTree = ""; }; 112B0EB52E30DD0F00562D6C /* MediaRemoteAdapterTestClient */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = MediaRemoteAdapterTestClient; sourceTree = ""; }; 112B0EBA2E30DD5000562D6C /* MediaRemoteAdapter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaRemoteAdapter.framework; path = "mediaremote-adapter/MediaRemoteAdapter.framework"; sourceTree = ""; }; 112FB7342CCF16F70015238C /* NotchSpaceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchSpaceManager.swift; sourceTree = ""; }; 1132E5102E777B6E0068732D /* YouTubeMusicModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YouTubeMusicModels.swift; sourceTree = ""; }; 1132E5132E777B920068732D /* YouTubeMusicNetworking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YouTubeMusicNetworking.swift; sourceTree = ""; }; 1132E5152E777C140068732D /* YouTubeMusicAuthentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YouTubeMusicAuthentication.swift; sourceTree = ""; }; 1153BD8D2D986B1F00979FB0 /* MediaControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaControllerProtocol.swift; sourceTree = ""; }; 1153BD902D986DB300979FB0 /* PlaybackState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackState.swift; sourceTree = ""; }; 1153BD922D986E4300979FB0 /* AppleMusicController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleMusicController.swift; sourceTree = ""; }; 1153BD972D9881F900979FB0 /* AppleScriptHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleScriptHelper.swift; sourceTree = ""; }; 1153BD992D98824300979FB0 /* SpotifyController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpotifyController.swift; sourceTree = ""; }; 1153BD9B2D98853B00979FB0 /* NowPlayingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingController.swift; sourceTree = ""; }; 1153BDA62D99B22200979FB0 /* YouTubeMusicController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YouTubeMusicController.swift; sourceTree = ""; }; 115C12EB2ED3D003009754CA /* OpenNotchHUD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenNotchHUD.swift; sourceTree = ""; }; 1160F8D72DD98230006FBB94 /* NotchShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchShape.swift; sourceTree = ""; }; 1163988C2DF5CAB40052E6AF /* EventModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventModel.swift; sourceTree = ""; }; 1163988E2DF5CC870052E6AF /* CalendarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarModel.swift; sourceTree = ""; }; 116398952DF5D6C00052E6AF /* CalendarServiceProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarServiceProviding.swift; sourceTree = ""; }; 118D1FD02E98FF5F00A2FF63 /* SharingStateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingStateManager.swift; sourceTree = ""; }; 118EBE242E92DCCB00D54B5A /* AssociatedObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssociatedObject.swift; sourceTree = ""; }; 118EBE262E92DE7400D54B5A /* NSMenu+AssociatedObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSMenu+AssociatedObject.swift"; sourceTree = ""; }; 118EBE282E946B3F00D54B5A /* ShareServiceFinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareServiceFinder.swift; sourceTree = ""; }; 118EBE2C2E97165600D54B5A /* Bookmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bookmark.swift; sourceTree = ""; }; 118EBE302E9717DB00D54B5A /* URL+SecurityScoped.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+SecurityScoped.swift"; sourceTree = ""; }; 118EBE3A2E9720C500D54B5A /* ShelfStateViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShelfStateViewModel.swift; sourceTree = ""; }; 1194E87B2EA19E09009C82D6 /* ImageProcessingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageProcessingService.swift; sourceTree = ""; }; 1194E8862EA6DDA7009C82D6 /* BoringNotchSkyLightWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoringNotchSkyLightWindow.swift; sourceTree = ""; }; 1194E93F2EACC652009C82D6 /* Color+AccentColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+AccentColor.swift"; sourceTree = ""; }; 11A45C782E34E63100CEB175 /* MediaChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaChecker.swift; sourceTree = ""; }; 11C5E3112DFE85970065821E /* SettingsWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsWindowController.swift; sourceTree = ""; }; 11C5E3152DFE88510065821E /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 11CC44A12CEE614100C7244B /* BoringViewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoringViewCoordinator.swift; sourceTree = ""; }; 11CFC65A2E097E9D00748C80 /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = ""; }; 11CFC65E2E097F2100748C80 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = ""; }; 11CFC6602E097F6800748C80 /* PermissionsRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionsRequestView.swift; sourceTree = ""; }; 11CFC6622E09917B00748C80 /* MusicControllerSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicControllerSelectionView.swift; sourceTree = ""; }; 11CFC6642E09C7B300748C80 /* OnboardingFinishView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingFinishView.swift; sourceTree = ""; }; 11D58EA12E760AE100FA8377 /* ImageService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageService.swift; sourceTree = ""; }; 11EFCD6F2E8E92D600D0B974 /* ShelfItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShelfItemViewModel.swift; sourceTree = ""; }; 11F747CD2EC75CEA00F841DB /* DragPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DragPreviewView.swift; sourceTree = ""; }; 11F7484F2EC9AABA00F841DB /* BoringNotchXPCHelper.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = BoringNotchXPCHelper.xpc; sourceTree = BUILT_PRODUCTS_DIR; }; 11F748652EC9AC9600F841DB /* BoringNotchXPCHelperProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoringNotchXPCHelperProtocol.swift; sourceTree = ""; }; 11F748662EC9AC9600F841DB /* XPCHelperClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XPCHelperClient.swift; sourceTree = ""; }; 11F748812ECB07A400F841DB /* MusicControlButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicControlButton.swift; sourceTree = ""; }; 11F748832ECB27DC00F841DB /* MusicSlotConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicSlotConfigurationView.swift; sourceTree = ""; }; 14288DD62C6E015000B9F80C /* AudioPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioPlayer.swift; sourceTree = ""; }; 14288DE72C6E01C800B9F80C /* ProgressIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressIndicator.swift; sourceTree = ""; }; 14288E0B2C6F8EC000B9F80C /* AppIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIcons.swift; sourceTree = ""; }; 1443E7F22C609DCE0027C1FC /* matters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = matters.swift; sourceTree = ""; }; 1443E7F42C609E650027C1FC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 147163972C5D35B70068B555 /* MusicVisualizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicVisualizer.swift; sourceTree = ""; }; 147163992C5D35FF0068B555 /* MusicManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicManager.swift; sourceTree = ""; }; 1471A8582C6281BD0058408D /* BoringNotchWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoringNotchWindow.swift; sourceTree = ""; }; 149E0B962C737D00006418B1 /* WebcamManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebcamManager.swift; sourceTree = ""; }; 149E0B992C737D40006418B1 /* WebcamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebcamView.swift; sourceTree = ""; }; 14A7E5872C64A89C008C1BE9 /* HelloAnimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelloAnimation.swift; sourceTree = ""; }; 14A7E5962C65FD31008C1BE9 /* boring.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = boring.m4a; sourceTree = ""; }; 14C08BB52C8DE42D000F8AA0 /* CalendarManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarManager.swift; sourceTree = ""; }; 14C08BB82C8DE4B1000F8AA0 /* BoringCalendar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoringCalendar.swift; sourceTree = ""; }; 14C08BC02C8E03AD000F8AA0 /* NSImage+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSImage+Extensions.swift"; sourceTree = ""; }; 14CEF4122C5CAED300855D72 /* boringNotch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = boringNotch.app; sourceTree = BUILT_PRODUCTS_DIR; }; 14CEF4152C5CAED300855D72 /* boringNotchApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = boringNotchApp.swift; sourceTree = ""; }; 14CEF4172C5CAED300855D72 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 14CEF4192C5CAED400855D72 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 14CEF41C2C5CAED400855D72 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 14CEF41E2C5CAED400855D72 /* boringNotch.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = boringNotch.entitlements; sourceTree = ""; }; 14D031ED2C689DB70096E6A1 /* IOKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOKit.framework; path = System/Library/Frameworks/IOKit.framework; sourceTree = SDKROOT; }; 14D031EF2C689DC00096E6A1 /* ApplicationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ApplicationServices.framework; path = System/Library/Frameworks/ApplicationServices.framework; sourceTree = SDKROOT; }; 14D570B82C5E98A20011E668 /* drop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = drop.swift; sourceTree = ""; }; 14D570BB2C5E98EB0011E668 /* generic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = generic.swift; sourceTree = ""; }; 14D570BF2C5EA5870011E668 /* AnimatedFace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedFace.swift; sourceTree = ""; }; 14D570C12C5EAFBF0011E668 /* EmptyState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyState.swift; sourceTree = ""; }; 14D570C52C5F38210011E668 /* BoringHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoringHeader.swift; sourceTree = ""; }; 14D570C82C5F38890011E668 /* BoringViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoringViewModel.swift; sourceTree = ""; }; 14D570CA2C5F4B2C0011E668 /* BatteryStatusViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryStatusViewModel.swift; sourceTree = ""; }; 14D570CC2C5F4BB70011E668 /* BoringBattery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoringBattery.swift; sourceTree = ""; }; 14D570D12C5F6C6A0011E668 /* BoringExtrasMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoringExtrasMenu.swift; sourceTree = ""; }; 14E9FEA92C70BF610062E83F /* DownloadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadView.swift; sourceTree = ""; }; 14E9FEAD2C7325770062E83F /* Button+Bouncing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Button+Bouncing.swift"; sourceTree = ""; }; 14FC6E4F2C7DED5600C7BEA5 /* DataTypes+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataTypes+Extensions.swift"; sourceTree = ""; }; 507266DA2C908E2E00A2D00D /* HoverButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoverButton.swift; sourceTree = ""; }; 5917FD102E57891600E87F1C /* MediaKeyInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaKeyInterceptor.swift; sourceTree = ""; }; 5955950C2E900ED800C66711 /* ApplicationRelauncher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationRelauncher.swift; sourceTree = ""; }; 59D8C23B2E589FAA00147B33 /* VolumeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeManager.swift; sourceTree = ""; }; 9A0887312C7A693000C160EA /* TabButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabButton.swift; sourceTree = ""; }; 9A0887342C7AFF8E00C160EA /* TabSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabSelectionView.swift; sourceTree = ""; }; 9A987A032C73CA66005CA465 /* ShelfView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShelfView.swift; sourceTree = ""; }; 9AB0C6BB2C73C9CB00F7CD30 /* NotchHomeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotchHomeView.swift; sourceTree = ""; }; B10348D82C74E56000475897 /* ConditionalModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalModifier.swift; sourceTree = ""; }; B10F84A22C6C9596009F3026 /* TestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestView.swift; sourceTree = ""; }; B141C2402CA5F53E00AC8CC8 /* SparkleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SparkleView.swift; sourceTree = ""; }; B17266DE2C64DFA00031BA0D /* BundleInfos.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleInfos.swift; sourceTree = ""; }; B17266E02C6532560031BA0D /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; B17266E22C65F7FB0031BA0D /* WhatsNewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhatsNewView.swift; sourceTree = ""; }; B172AABF2C95DA0B001623F1 /* InlineHUD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineHUD.swift; sourceTree = ""; }; B186543B2C6F49AE000B926A /* ShortcutConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutConstants.swift; sourceTree = ""; }; B19016232CC15B4D00E3F12E /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; B19424082CD0FEFE003E5DC2 /* LottieAnimationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieAnimationView.swift; sourceTree = ""; }; B1A78C812C8BA08100BD51B0 /* FullscreenMediaDetection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullscreenMediaDetection.swift; sourceTree = ""; }; B1B112902C6A572100093D8F /* EditPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditPanelView.swift; sourceTree = ""; }; B1B112922C6A577E00093D8F /* MouseTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MouseTracker.swift; sourceTree = ""; }; B1C448952C9712C4001F0858 /* ActionBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionBar.swift; sourceTree = ""; }; B1C448972C972CC4001F0858 /* ListItemPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListItemPopover.swift; sourceTree = ""; }; B1C4489A2C97376A001F0858 /* TipStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipStore.swift; sourceTree = ""; }; B1C974332C642B6D0000E707 /* MarqueeTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarqueeTextView.swift; sourceTree = ""; }; B1CE8CFD2C6F659400DD9871 /* KeyboardShortcutsHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardShortcutsHelper.swift; sourceTree = ""; }; B1D365CD2C6A979C0047BDBC /* LiveActivityModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityModifier.swift; sourceTree = ""; }; B1D365CF2C6A9A6C0047BDBC /* SystemEventIndicatorModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemEventIndicatorModifier.swift; sourceTree = ""; }; B1D6FD422C6603730015F173 /* SoftwareUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftwareUpdater.swift; sourceTree = ""; }; B1F0A0012E60000100000001 /* BrightnessManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrightnessManager.swift; sourceTree = ""; }; B1F747F82EC7E94000F841DB /* LottieView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieView.swift; sourceTree = ""; }; B1FEB4982C7686630066EBBC /* PanGesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PanGesture.swift; sourceTree = ""; }; F38DE6472D8243E2008B5C6D /* BatteryActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryActivityManager.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ 11F7485C2EC9AABA00F841DB /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( Info.plist, ); target = 11F7484E2EC9AABA00F841DB /* BoringNotchXPCHelper */; }; /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ 112FB72F2CCF12CC0015238C /* private */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = private; sourceTree = ""; }; 11F748502EC9AABA00F841DB /* BoringNotchXPCHelper */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (11F7485C2EC9AABA00F841DB /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = BoringNotchXPCHelper; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ 11F7484C2EC9AABA00F841DB /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 14CEF40F2C5CAED300855D72 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 111BEA5F2ED07A340079DD4E /* MacroVisionKit in Frameworks */, 9A987A102C73CA8D005CA465 /* Collections in Frameworks */, 1194E8852EA57D23009C82D6 /* SkyLightWindow in Frameworks */, 112B0EBB2E30DD5000562D6C /* MediaRemoteAdapter.framework in Frameworks */, 14D0321D2C68F3350096E6A1 /* Sparkle in Frameworks */, 111BEA6F2ED166E20079DD4E /* MacroVisionKit in Frameworks */, 11F748732EC9DA9300F841DB /* Lottie in Frameworks */, 14D0321A2C68F32E0096E6A1 /* LaunchAtLogin in Frameworks */, 111BEA512ECFBF7F0079DD4E /* MacroVisionKit in Frameworks */, B18654392C6F4990000B926A /* KeyboardShortcuts in Frameworks */, B19016222CC15B3D00E3F12E /* Defaults in Frameworks */, 111BE95D2ECD71E10079DD4E /* AsyncXPCConnection in Frameworks */, B1628B922CC260C0003D8DF3 /* SwiftUIIntrospect in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 1113ABB82E80E27000EC13B2 /* Models */ = { isa = PBXGroup; children = ( 118EBE2C2E97165600D54B5A /* Bookmark.swift */, 1113ABB72E80E27000EC13B2 /* ShelfItem.swift */, ); path = Models; sourceTree = ""; }; 1113ABBF2E80E27000EC13B2 /* Services */ = { isa = PBXGroup; children = ( 1194E87B2EA19E09009C82D6 /* ImageProcessingService.swift */, 118EBE282E946B3F00D54B5A /* ShareServiceFinder.swift */, 1100292D2E86940F00035A57 /* QuickShareService.swift */, 110029262E84FD4C00035A57 /* TemporaryFileStorageService.swift */, 1113ABCF2E80E6BB00EC13B2 /* ThumbnailService.swift */, 1113ABBA2E80E27000EC13B2 /* QuickLookService.swift */, 1113ABBC2E80E27000EC13B2 /* ShelfActionService.swift */, 1113ABBD2E80E27000EC13B2 /* ShelfDropService.swift */, 1113ABBE2E80E27000EC13B2 /* ShelfPersistenceService.swift */, ); path = Services; sourceTree = ""; }; 1113ABC22E80E27000EC13B2 /* ViewModels */ = { isa = PBXGroup; children = ( 118EBE3A2E9720C500D54B5A /* ShelfStateViewModel.swift */, 11EFCD6F2E8E92D600D0B974 /* ShelfItemViewModel.swift */, 1113ABC02E80E27000EC13B2 /* ShelfSelectionModel.swift */, ); path = ViewModels; sourceTree = ""; }; 1113ABC42E80E27000EC13B2 /* Views */ = { isa = PBXGroup; children = ( 11F747CD2EC75CEA00F841DB /* DragPreviewView.swift */, 110029292E8691B400035A57 /* FileShareView.swift */, 1113ABC32E80E27000EC13B2 /* ShelfItemView.swift */, 9A987A032C73CA66005CA465 /* ShelfView.swift */, ); path = Views; sourceTree = ""; }; 112B0EB62E30DD0F00562D6C /* mediaremote-adapter */ = { isa = PBXGroup; children = ( 112B0EB32E30DD0F00562D6C /* mediaremote-adapter.pl */, 112B0EB52E30DD0F00562D6C /* MediaRemoteAdapterTestClient */, ); path = "mediaremote-adapter"; sourceTree = ""; }; 1132E5232E78D6DA0068732D /* YouTube Music Controller */ = { isa = PBXGroup; children = ( 1132E5152E777C140068732D /* YouTubeMusicAuthentication.swift */, 1132E5132E777B920068732D /* YouTubeMusicNetworking.swift */, 1132E5102E777B6E0068732D /* YouTubeMusicModels.swift */, 1153BDA62D99B22200979FB0 /* YouTubeMusicController.swift */, ); path = "YouTube Music Controller"; sourceTree = ""; }; 1153BD8E2D986B1F00979FB0 /* MediaControllers */ = { isa = PBXGroup; children = ( 1132E5232E78D6DA0068732D /* YouTube Music Controller */, 1153BD8D2D986B1F00979FB0 /* MediaControllerProtocol.swift */, 1153BD922D986E4300979FB0 /* AppleMusicController.swift */, 1153BD992D98824300979FB0 /* SpotifyController.swift */, 1153BD9B2D98853B00979FB0 /* NowPlayingController.swift */, ); path = MediaControllers; sourceTree = ""; }; 116398942DF5D6B40052E6AF /* Providers */ = { isa = PBXGroup; children = ( 116398952DF5D6C00052E6AF /* CalendarServiceProviding.swift */, ); path = Providers; sourceTree = ""; }; 11F748672EC9AC9600F841DB /* XPCHelperClient */ = { isa = PBXGroup; children = ( 11F748652EC9AC9600F841DB /* BoringNotchXPCHelperProtocol.swift */, 11F748662EC9AC9600F841DB /* XPCHelperClient.swift */, ); path = XPCHelperClient; sourceTree = ""; }; 14288DD92C6E015000B9F80C /* helpers */ = { isa = PBXGroup; children = ( 118EBE242E92DCCB00D54B5A /* AssociatedObject.swift */, 11A45C782E34E63100CEB175 /* MediaChecker.swift */, 1153BD972D9881F900979FB0 /* AppleScriptHelper.swift */, 14288DD62C6E015000B9F80C /* AudioPlayer.swift */, 5955950C2E900ED800C66711 /* ApplicationRelauncher.swift */, 14288E0B2C6F8EC000B9F80C /* AppIcons.swift */, ); path = helpers; sourceTree = ""; }; 14288DE22C6E016F00B9F80C /* observers */ = { isa = PBXGroup; children = ( 111BE9942ECF2DEF0079DD4E /* DragDetector.swift */, B1A78C812C8BA08100BD51B0 /* FullscreenMediaDetection.swift */, 5917FD102E57891600E87F1C /* MediaKeyInterceptor.swift */, ); path = observers; sourceTree = ""; }; 1443E7F12C609DB90027C1FC /* sizing */ = { isa = PBXGroup; children = ( 1443E7F22C609DCE0027C1FC /* matters.swift */, ); path = sizing; sourceTree = ""; }; 1471639B2C5D362F0068B555 /* components */ = { isa = PBXGroup; children = ( B141C23B2CA5F50900AC8CC8 /* Onboarding */, B1C448992C97375A001F0858 /* Tips */, 14C08BB72C8DE49E000F8AA0 /* Calendar */, 9A987A042C73CA66005CA465 /* Shelf */, 149E0B982C737D26006418B1 /* Webcam */, B18654312C6F45AE000B926A /* Live activities */, B18654302C6F4590000B926A /* Settings */, B186542F2C6F455E000B926A /* Notch */, 9A0887332C7AFF7E00C160EA /* Tabs */, B186542E2C6F453B000B926A /* Music */, 14D570BF2C5EA5870011E668 /* AnimatedFace.swift */, 14288DE72C6E01C800B9F80C /* ProgressIndicator.swift */, 14D570C12C5EAFBF0011E668 /* EmptyState.swift */, B17266E22C65F7FB0031BA0D /* WhatsNewView.swift */, B10F84A22C6C9596009F3026 /* TestView.swift */, 507266DA2C908E2E00A2D00D /* HoverButton.swift */, B1F747F82EC7E94000F841DB /* LottieView.swift */, ); path = components; sourceTree = ""; }; 147163B52C5D804B0068B555 /* managers */ = { isa = PBXGroup; children = ( 11D58EA12E760AE100FA8377 /* ImageService.swift */, F38DE6472D8243E2008B5C6D /* BatteryActivityManager.swift */, 112FB7342CCF16F70015238C /* NotchSpaceManager.swift */, 147163992C5D35FF0068B555 /* MusicManager.swift */, 149E0B962C737D00006418B1 /* WebcamManager.swift */, 14C08BB52C8DE42D000F8AA0 /* CalendarManager.swift */, B1F0A0012E60000100000001 /* BrightnessManager.swift */, 59D8C23B2E589FAA00147B33 /* VolumeManager.swift */, ); path = managers; sourceTree = ""; }; 149E0B982C737D26006418B1 /* Webcam */ = { isa = PBXGroup; children = ( 149E0B992C737D40006418B1 /* WebcamView.swift */, ); path = Webcam; sourceTree = ""; }; 14C08BB72C8DE49E000F8AA0 /* Calendar */ = { isa = PBXGroup; children = ( 14C08BB82C8DE4B1000F8AA0 /* BoringCalendar.swift */, ); path = Calendar; sourceTree = ""; }; 14CEF4092C5CAED200855D72 = { isa = PBXGroup; children = ( 112B0EB62E30DD0F00562D6C /* mediaremote-adapter */, 14CEF4142C5CAED300855D72 /* boringNotch */, 11F748502EC9AABA00F841DB /* BoringNotchXPCHelper */, 14CEF4132C5CAED300855D72 /* Products */, 14D031EC2C689DB70096E6A1 /* Frameworks */, ); sourceTree = ""; }; 14CEF4132C5CAED300855D72 /* Products */ = { isa = PBXGroup; children = ( 14CEF4122C5CAED300855D72 /* boringNotch.app */, 11F7484F2EC9AABA00F841DB /* BoringNotchXPCHelper.xpc */, ); name = Products; sourceTree = ""; }; 14CEF4142C5CAED300855D72 /* boringNotch */ = { isa = PBXGroup; children = ( 11F748672EC9AC9600F841DB /* XPCHelperClient */, 116398942DF5D6B40052E6AF /* Providers */, 14288DE22C6E016F00B9F80C /* observers */, 14288DD92C6E015000B9F80C /* helpers */, B186543A2C6F49A4000B926A /* Shortcuts */, 1443E7F12C609DB90027C1FC /* sizing */, 14D570C72C5F38760011E668 /* models */, 14D570BA2C5E98E30011E668 /* enums */, 14D570B72C5E98960011E668 /* animations */, 147163B52C5D804B0068B555 /* managers */, 1153BD8E2D986B1F00979FB0 /* MediaControllers */, B15063502C63D3F600EBB0E3 /* extensions */, 1471639B2C5D362F0068B555 /* components */, 14CEF4152C5CAED300855D72 /* boringNotchApp.swift */, 14CEF4172C5CAED300855D72 /* ContentView.swift */, B17266E02C6532560031BA0D /* Localizable.xcstrings */, 14CEF4192C5CAED400855D72 /* Assets.xcassets */, 14A7E5962C65FD31008C1BE9 /* boring.m4a */, 1443E7F42C609E650027C1FC /* Info.plist */, 14CEF41E2C5CAED400855D72 /* boringNotch.entitlements */, 14CEF41B2C5CAED400855D72 /* Preview Content */, 112FB72F2CCF12CC0015238C /* private */, 11CC44A12CEE614100C7244B /* BoringViewCoordinator.swift */, ); path = boringNotch; sourceTree = ""; }; 14CEF41B2C5CAED400855D72 /* Preview Content */ = { isa = PBXGroup; children = ( 14CEF41C2C5CAED400855D72 /* Preview Assets.xcassets */, ); path = "Preview Content"; sourceTree = ""; }; 14D031EC2C689DB70096E6A1 /* Frameworks */ = { isa = PBXGroup; children = ( 14D031EF2C689DC00096E6A1 /* ApplicationServices.framework */, 14D031ED2C689DB70096E6A1 /* IOKit.framework */, 112B0EBA2E30DD5000562D6C /* MediaRemoteAdapter.framework */, ); name = Frameworks; sourceTree = ""; }; 14D570B72C5E98960011E668 /* animations */ = { isa = PBXGroup; children = ( 14D570B82C5E98A20011E668 /* drop.swift */, 14A7E5872C64A89C008C1BE9 /* HelloAnimation.swift */, ); path = animations; sourceTree = ""; }; 14D570BA2C5E98E30011E668 /* enums */ = { isa = PBXGroup; children = ( 14D570BB2C5E98EB0011E668 /* generic.swift */, ); path = enums; sourceTree = ""; }; 14D570C72C5F38760011E668 /* models */ = { isa = PBXGroup; children = ( 11F748812ECB07A400F841DB /* MusicControlButton.swift */, 118D1FD02E98FF5F00A2FF63 /* SharingStateManager.swift */, 1163988E2DF5CC870052E6AF /* CalendarModel.swift */, 1163988C2DF5CAB40052E6AF /* EventModel.swift */, B19016232CC15B4D00E3F12E /* Constants.swift */, 14D570C82C5F38890011E668 /* BoringViewModel.swift */, 14D570CA2C5F4B2C0011E668 /* BatteryStatusViewModel.swift */, 1153BD902D986DB300979FB0 /* PlaybackState.swift */, ); path = models; sourceTree = ""; }; 9A0887332C7AFF7E00C160EA /* Tabs */ = { isa = PBXGroup; children = ( 9A0887312C7A693000C160EA /* TabButton.swift */, 9A0887342C7AFF8E00C160EA /* TabSelectionView.swift */, ); path = Tabs; sourceTree = ""; }; 9A987A042C73CA66005CA465 /* Shelf */ = { isa = PBXGroup; children = ( 1113ABB82E80E27000EC13B2 /* Models */, 1113ABBF2E80E27000EC13B2 /* Services */, 1113ABC22E80E27000EC13B2 /* ViewModels */, 1113ABC42E80E27000EC13B2 /* Views */, ); path = Shelf; sourceTree = ""; }; B141C23B2CA5F50900AC8CC8 /* Onboarding */ = { isa = PBXGroup; children = ( 11CFC6642E09C7B300748C80 /* OnboardingFinishView.swift */, 11CFC6622E09917B00748C80 /* MusicControllerSelectionView.swift */, 11CFC6602E097F6800748C80 /* PermissionsRequestView.swift */, 11CFC65E2E097F2100748C80 /* OnboardingView.swift */, 11CFC65A2E097E9D00748C80 /* WelcomeView.swift */, B141C2402CA5F53E00AC8CC8 /* SparkleView.swift */, ); path = Onboarding; sourceTree = ""; }; B15063502C63D3F600EBB0E3 /* extensions */ = { isa = PBXGroup; children = ( 111BEA602ED09B1B0079DD4E /* NSScreen+UUID.swift */, 1194E93F2EACC652009C82D6 /* Color+AccentColor.swift */, 118EBE302E9717DB00D54B5A /* URL+SecurityScoped.swift */, 118EBE262E92DE7400D54B5A /* NSMenu+AssociatedObject.swift */, 1100290B2E847E2800035A57 /* NSItemProvider+LoadHelpers.swift */, 14C08BC02C8E03AD000F8AA0 /* NSImage+Extensions.swift */, B17266DE2C64DFA00031BA0D /* BundleInfos.swift */, B1B112922C6A577E00093D8F /* MouseTracker.swift */, B1CE8CFD2C6F659400DD9871 /* KeyboardShortcutsHelper.swift */, 14E9FEAD2C7325770062E83F /* Button+Bouncing.swift */, B10348D82C74E56000475897 /* ConditionalModifier.swift */, B1FEB4982C7686630066EBBC /* PanGesture.swift */, 14FC6E4F2C7DED5600C7BEA5 /* DataTypes+Extensions.swift */, B1C448952C9712C4001F0858 /* ActionBar.swift */, ); path = extensions; sourceTree = ""; }; B186542E2C6F453B000B926A /* Music */ = { isa = PBXGroup; children = ( B19424082CD0FEFE003E5DC2 /* LottieAnimationView.swift */, 147163972C5D35B70068B555 /* MusicVisualizer.swift */, ); path = Music; sourceTree = ""; }; B186542F2C6F455E000B926A /* Notch */ = { isa = PBXGroup; children = ( 1194E8862EA6DDA7009C82D6 /* BoringNotchSkyLightWindow.swift */, 1160F8D72DD98230006FBB94 /* NotchShape.swift */, 9AB0C6BB2C73C9CB00F7CD30 /* NotchHomeView.swift */, 14D570C52C5F38210011E668 /* BoringHeader.swift */, 14D570D12C5F6C6A0011E668 /* BoringExtrasMenu.swift */, 1471A8582C6281BD0058408D /* BoringNotchWindow.swift */, ); path = Notch; sourceTree = ""; }; B18654302C6F4590000B926A /* Settings */ = { isa = PBXGroup; children = ( 11F748832ECB27DC00F841DB /* MusicSlotConfigurationView.swift */, 11C5E3152DFE88510065821E /* SettingsView.swift */, 11C5E3112DFE85970065821E /* SettingsWindowController.swift */, B1D6FD422C6603730015F173 /* SoftwareUpdater.swift */, B1B112902C6A572100093D8F /* EditPanelView.swift */, B1C448972C972CC4001F0858 /* ListItemPopover.swift */, ); path = Settings; sourceTree = ""; }; B18654312C6F45AE000B926A /* Live activities */ = { isa = PBXGroup; children = ( 115C12EB2ED3D003009754CA /* OpenNotchHUD.swift */, 14D570CC2C5F4BB70011E668 /* BoringBattery.swift */, B1C974332C642B6D0000E707 /* MarqueeTextView.swift */, B1D365CD2C6A979C0047BDBC /* LiveActivityModifier.swift */, B1D365CF2C6A9A6C0047BDBC /* SystemEventIndicatorModifier.swift */, 14E9FEA92C70BF610062E83F /* DownloadView.swift */, B172AABF2C95DA0B001623F1 /* InlineHUD.swift */, ); path = "Live activities"; sourceTree = ""; }; B186543A2C6F49A4000B926A /* Shortcuts */ = { isa = PBXGroup; children = ( B186543B2C6F49AE000B926A /* ShortcutConstants.swift */, ); path = Shortcuts; sourceTree = ""; }; B1C448992C97375A001F0858 /* Tips */ = { isa = PBXGroup; children = ( B1C4489A2C97376A001F0858 /* TipStore.swift */, ); path = Tips; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 11F7484E2EC9AABA00F841DB /* BoringNotchXPCHelper */ = { isa = PBXNativeTarget; buildConfigurationList = 11F7485D2EC9AABA00F841DB /* Build configuration list for PBXNativeTarget "BoringNotchXPCHelper" */; buildPhases = ( 11F7484B2EC9AABA00F841DB /* Sources */, 11F7484C2EC9AABA00F841DB /* Frameworks */, 11F7484D2EC9AABA00F841DB /* Resources */, ); buildRules = ( ); dependencies = ( ); fileSystemSynchronizedGroups = ( 11F748502EC9AABA00F841DB /* BoringNotchXPCHelper */, ); name = BoringNotchXPCHelper; packageProductDependencies = ( ); productName = BoringNotchXPCHelper; productReference = 11F7484F2EC9AABA00F841DB /* BoringNotchXPCHelper.xpc */; productType = "com.apple.product-type.xpc-service"; }; 14CEF4112C5CAED300855D72 /* boringNotch */ = { isa = PBXNativeTarget; buildConfigurationList = 14CEF4212C5CAED400855D72 /* Build configuration list for PBXNativeTarget "boringNotch" */; buildPhases = ( 14CEF40E2C5CAED300855D72 /* Sources */, 14CEF40F2C5CAED300855D72 /* Frameworks */, 14CEF4102C5CAED300855D72 /* Resources */, B1ECFA062C6FE58A002ACD87 /* Embed Frameworks */, 11F748412EC8051E00F841DB /* Embed XPC Services */, ); buildRules = ( ); dependencies = ( 11F7485A2EC9AABA00F841DB /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( 112FB72F2CCF12CC0015238C /* private */, ); name = boringNotch; packageProductDependencies = ( 14D032192C68F32E0096E6A1 /* LaunchAtLogin */, 14D0321C2C68F3350096E6A1 /* Sparkle */, B18654382C6F4990000B926A /* KeyboardShortcuts */, 9A987A0F2C73CA8D005CA465 /* Collections */, B19016212CC15B3D00E3F12E /* Defaults */, B1628B912CC260C0003D8DF3 /* SwiftUIIntrospect */, 1194E8842EA57D23009C82D6 /* SkyLightWindow */, 11F748722EC9DA9300F841DB /* Lottie */, 111BE95C2ECD71E10079DD4E /* AsyncXPCConnection */, 111BEA502ECFBF7F0079DD4E /* MacroVisionKit */, 111BEA5E2ED07A340079DD4E /* MacroVisionKit */, 111BEA6E2ED166E20079DD4E /* MacroVisionKit */, ); productName = dynamicNotch; productReference = 14CEF4122C5CAED300855D72 /* boringNotch.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 14CEF40A2C5CAED200855D72 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1640; LastUpgradeCheck = 1640; TargetAttributes = { 11F7484E2EC9AABA00F841DB = { CreatedOnToolsVersion = 16.4; }; 14CEF4112C5CAED300855D72 = { CreatedOnToolsVersion = 15.4; LastSwiftMigration = 1540; }; }; }; buildConfigurationList = 14CEF40D2C5CAED200855D72 /* Build configuration list for PBXProject "boringNotch" */; compatibilityVersion = "Xcode 14.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 14CEF4092C5CAED200855D72; packageReferences = ( 14D032182C68F32E0096E6A1 /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */, 14D0321B2C68F3350096E6A1 /* XCRemoteSwiftPackageReference "Sparkle" */, B18654372C6F4990000B926A /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */, 9A987A0E2C73CA8D005CA465 /* XCRemoteSwiftPackageReference "swift-collections" */, 9A987A112C73CAA1005CA465 /* XCRemoteSwiftPackageReference "Pow" */, B19016202CC15B3D00E3F12E /* XCRemoteSwiftPackageReference "Defaults" */, B1628B902CC260C0003D8DF3 /* XCRemoteSwiftPackageReference "swiftui-introspect" */, 1194E8832EA57D23009C82D6 /* XCRemoteSwiftPackageReference "SkyLightWindow" */, 11F748712EC9DA9300F841DB /* XCRemoteSwiftPackageReference "lottie-spm" */, 111BE95B2ECD71E10079DD4E /* XCRemoteSwiftPackageReference "AsyncXPCConnection" */, 111BEA6D2ED166E20079DD4E /* XCRemoteSwiftPackageReference "MacroVisionKit" */, ); productRefGroup = 14CEF4132C5CAED300855D72 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 14CEF4112C5CAED300855D72 /* boringNotch */, 11F7484E2EC9AABA00F841DB /* BoringNotchXPCHelper */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 11F7484D2EC9AABA00F841DB /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 14CEF4102C5CAED300855D72 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 14CEF41D2C5CAED400855D72 /* Preview Assets.xcassets in Resources */, 14CEF41A2C5CAED400855D72 /* Assets.xcassets in Resources */, 112B0EB82E30DD0F00562D6C /* MediaRemoteAdapterTestClient in Resources */, 112B0EB92E30DD0F00562D6C /* mediaremote-adapter.pl in Resources */, B17266E12C6532560031BA0D /* Localizable.xcstrings in Resources */, 14A7E5972C65FD31008C1BE9 /* boring.m4a in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 11F7484B2EC9AABA00F841DB /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 14CEF40E2C5CAED300855D72 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 115C12EC2ED3D003009754CA /* OpenNotchHUD.swift in Sources */, 14C08BB92C8DE4B1000F8AA0 /* BoringCalendar.swift in Sources */, 5955950D2E900ED800C66711 /* ApplicationRelauncher.swift in Sources */, 1100292A2E8691B400035A57 /* FileShareView.swift in Sources */, 147163982C5D35B70068B555 /* MusicVisualizer.swift in Sources */, 11CC44A22CEE614100C7244B /* BoringViewCoordinator.swift in Sources */, B186543C2C6F49AE000B926A /* ShortcutConstants.swift in Sources */, 11A45C792E34E63100CEB175 /* MediaChecker.swift in Sources */, B1D365CE2C6A979C0047BDBC /* LiveActivityModifier.swift in Sources */, 1113ABD02E80E6BB00EC13B2 /* ThumbnailService.swift in Sources */, 11CFC65B2E097E9D00748C80 /* WelcomeView.swift in Sources */, 14D570C02C5EA5870011E668 /* AnimatedFace.swift in Sources */, B1D6FD432C6603730015F173 /* SoftwareUpdater.swift in Sources */, 111BE9952ECF2DF40079DD4E /* DragDetector.swift in Sources */, B1D365D02C6A9A6C0047BDBC /* SystemEventIndicatorModifier.swift in Sources */, 1194E87C2EA19E09009C82D6 /* ImageProcessingService.swift in Sources */, 1194E8872EA6DDA7009C82D6 /* BoringNotchSkyLightWindow.swift in Sources */, 14D570CB2C5F4B2C0011E668 /* BatteryStatusViewModel.swift in Sources */, 9A0887322C7A693000C160EA /* TabButton.swift in Sources */, 1153BD9C2D98853B00979FB0 /* NowPlayingController.swift in Sources */, 1194E9402EACC652009C82D6 /* Color+AccentColor.swift in Sources */, B141C2412CA5F53F00AC8CC8 /* SparkleView.swift in Sources */, 116398962DF5D6C00052E6AF /* CalendarServiceProviding.swift in Sources */, 59D8C23C2E589FAA00147B33 /* VolumeManager.swift in Sources */, 1163988D2DF5CAB40052E6AF /* EventModel.swift in Sources */, 14D570B92C5E98A20011E668 /* drop.swift in Sources */, 11EFCD702E8E92D600D0B974 /* ShelfItemViewModel.swift in Sources */, 1153BDA72D99B22200979FB0 /* YouTubeMusicController.swift in Sources */, 1163988F2DF5CC870052E6AF /* CalendarModel.swift in Sources */, 1153BD9A2D98824300979FB0 /* SpotifyController.swift in Sources */, 118EBE292E946B3F00D54B5A /* ShareServiceFinder.swift in Sources */, B19424092CD0FF01003E5DC2 /* LottieAnimationView.swift in Sources */, B1F747F92EC7E94000F841DB /* LottieView.swift in Sources */, B1C974342C642B6D0000E707 /* MarqueeTextView.swift in Sources */, 14288DE82C6E01C800B9F80C /* ProgressIndicator.swift in Sources */, 1113ABC52E80E27000EC13B2 /* ShelfItemView.swift in Sources */, 1113ABC62E80E27000EC13B2 /* ShelfPersistenceService.swift in Sources */, 1113ABC82E80E27000EC13B2 /* ShelfItem.swift in Sources */, 1113ABCA2E80E27000EC13B2 /* ShelfSelectionModel.swift in Sources */, 1113ABCB2E80E27000EC13B2 /* QuickLookService.swift in Sources */, 1113ABCC2E80E27000EC13B2 /* ShelfDropService.swift in Sources */, 1100292E2E86940F00035A57 /* QuickShareService.swift in Sources */, 1113ABCE2E80E27000EC13B2 /* ShelfActionService.swift in Sources */, 1160F8D82DD98230006FBB94 /* NotchShape.swift in Sources */, 5917FD112E57891600E87F1C /* MediaKeyInterceptor.swift in Sources */, 14C08BC12C8E03AD000F8AA0 /* NSImage+Extensions.swift in Sources */, 149E0B9A2C737D40006418B1 /* WebcamView.swift in Sources */, 1471639A2C5D35FF0068B555 /* MusicManager.swift in Sources */, B1B112932C6A577E00093D8F /* MouseTracker.swift in Sources */, B1C448962C9712C4001F0858 /* ActionBar.swift in Sources */, 118EBE312E9717DB00D54B5A /* URL+SecurityScoped.swift in Sources */, 1132E5162E777C140068732D /* YouTubeMusicAuthentication.swift in Sources */, 14A7E5882C64A89C008C1BE9 /* HelloAnimation.swift in Sources */, 111BEA612ED09B1B0079DD4E /* NSScreen+UUID.swift in Sources */, 1153BD8F2D986B1F00979FB0 /* MediaControllerProtocol.swift in Sources */, 9AB0C6BD2C73C9CB00F7CD30 /* NotchHomeView.swift in Sources */, B172AAC02C95DA0B001623F1 /* InlineHUD.swift in Sources */, 14E9FEAA2C70BF610062E83F /* DownloadView.swift in Sources */, B1B112912C6A572100093D8F /* EditPanelView.swift in Sources */, B1C4489B2C97376A001F0858 /* TipStore.swift in Sources */, 1153BD912D986DB300979FB0 /* PlaybackState.swift in Sources */, B1A78C822C8BA08100BD51B0 /* FullscreenMediaDetection.swift in Sources */, 14E9FEAE2C7325770062E83F /* Button+Bouncing.swift in Sources */, 14D570C92C5F38890011E668 /* BoringViewModel.swift in Sources */, 14D570BC2C5E98EB0011E668 /* generic.swift in Sources */, 118EBE272E92DE8400D54B5A /* NSMenu+AssociatedObject.swift in Sources */, B1CE8CFE2C6F659400DD9871 /* KeyboardShortcutsHelper.swift in Sources */, 14D570C62C5F38210011E668 /* BoringHeader.swift in Sources */, B17266E32C65F7FB0031BA0D /* WhatsNewView.swift in Sources */, 14C08BB62C8DE42D000F8AA0 /* CalendarManager.swift in Sources */, 14D570CD2C5F4BB70011E668 /* BoringBattery.swift in Sources */, B10F84A32C6C9596009F3026 /* TestView.swift in Sources */, 1443E7F32C609DCE0027C1FC /* matters.swift in Sources */, 11C5E3162DFE88510065821E /* SettingsView.swift in Sources */, 1153BD932D986E4300979FB0 /* AppleMusicController.swift in Sources */, 11C5E3132DFE85970065821E /* SettingsWindowController.swift in Sources */, 110029272E84FD4C00035A57 /* TemporaryFileStorageService.swift in Sources */, 11CFC6652E09C7B300748C80 /* OnboardingFinishView.swift in Sources */, 507266DB2C908E2E00A2D00D /* HoverButton.swift in Sources */, 1471A8592C6281BD0058408D /* BoringNotchWindow.swift in Sources */, 14CEF4182C5CAED300855D72 /* ContentView.swift in Sources */, 9A987A0D2C73CA66005CA465 /* ShelfView.swift in Sources */, 1132E5142E777B920068732D /* YouTubeMusicNetworking.swift in Sources */, 1132E5122E777B6E0068732D /* YouTubeMusicModels.swift in Sources */, 11CFC65F2E097F2F00748C80 /* OnboardingView.swift in Sources */, 14CEF4162C5CAED300855D72 /* boringNotchApp.swift in Sources */, B17266DF2C64DFA00031BA0D /* BundleInfos.swift in Sources */, 118EBE3B2E9720C500D54B5A /* ShelfStateViewModel.swift in Sources */, 11F747CE2EC75CEA00F841DB /* DragPreviewView.swift in Sources */, F38DE6482D8243E7008B5C6D /* BatteryActivityManager.swift in Sources */, B10348D92C74E56000475897 /* ConditionalModifier.swift in Sources */, 118D1FD12E98FF5F00A2FF63 /* SharingStateManager.swift in Sources */, 14D570C22C5EAFBF0011E668 /* EmptyState.swift in Sources */, 118EBE252E92DCCB00D54B5A /* AssociatedObject.swift in Sources */, 11F748682EC9AC9600F841DB /* BoringNotchXPCHelperProtocol.swift in Sources */, 11F748692EC9AC9600F841DB /* XPCHelperClient.swift in Sources */, 118EBE2D2E97165600D54B5A /* Bookmark.swift in Sources */, B1FEB4992C7686630066EBBC /* PanGesture.swift in Sources */, 11F748842ECB27DC00F841DB /* MusicSlotConfigurationView.swift in Sources */, 9A0887352C7AFF8E00C160EA /* TabSelectionView.swift in Sources */, 112FB7352CCF16F70015238C /* NotchSpaceManager.swift in Sources */, 14FC6E502C7DED5600C7BEA5 /* DataTypes+Extensions.swift in Sources */, 1153BD982D9881F900979FB0 /* AppleScriptHelper.swift in Sources */, 11CFC6612E097F6800748C80 /* PermissionsRequestView.swift in Sources */, 14D570D22C5F6C6A0011E668 /* BoringExtrasMenu.swift in Sources */, 11D58EA22E760AE100FA8377 /* ImageService.swift in Sources */, 11F748822ECB07A400F841DB /* MusicControlButton.swift in Sources */, 14288DDC2C6E015000B9F80C /* AudioPlayer.swift in Sources */, 149E0B972C737D00006418B1 /* WebcamManager.swift in Sources */, 14288E0C2C6F8EC000B9F80C /* AppIcons.swift in Sources */, B1C448982C972CC4001F0858 /* ListItemPopover.swift in Sources */, B19016242CC15B5000E3F12E /* Constants.swift in Sources */, 1100290C2E847E2800035A57 /* NSItemProvider+LoadHelpers.swift in Sources */, 11CFC6632E09918400748C80 /* MusicControllerSelectionView.swift in Sources */, B1F0A0022E60000100000001 /* BrightnessManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 11F7485A2EC9AABA00F841DB /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 11F7484E2EC9AABA00F841DB /* BoringNotchXPCHelper */; targetProxy = 11F748592EC9AABA00F841DB /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ 11F7485E2EC9AABA00F841DB /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_ENTITLEMENTS = BoringNotchXPCHelper/BoringNotchXPCHelper.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 271; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = BoringNotchXPCHelper/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = BoringNotchXPCHelper; INFOPLIST_KEY_NSHumanReadableCopyright = ""; MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 2.7.3; PRODUCT_BUNDLE_IDENTIFIER = theboringteam.boringnotch.BoringNotchXPCHelper; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; name = Debug; }; 11F7485F2EC9AABA00F841DB /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_ENTITLEMENTS = BoringNotchXPCHelper/BoringNotchXPCHelper.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 271; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = BoringNotchXPCHelper/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = BoringNotchXPCHelper; INFOPLIST_KEY_NSHumanReadableCopyright = ""; MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 2.7.3; PRODUCT_BUNDLE_IDENTIFIER = theboringteam.boringnotch.BoringNotchXPCHelper; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; name = Release; }; 14CEF41F2C5CAED400855D72 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MACOSX_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = targeted; SWIFT_VERSION = 5.0; }; name = Debug; }; 14CEF4202C5CAED400855D72 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MACOSX_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_STRICT_CONCURRENCY = targeted; SWIFT_VERSION = 5.0; }; name = Release; }; 14CEF4222C5CAED400855D72 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES; CODE_SIGN_ENTITLEMENTS = boringNotch/boringNotch.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 271; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"boringNotch/Preview Content\""; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/mediaremote-adapter", ); GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = boringNotch/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TheBoringNotch; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSBackgroundOnly = NO; INFOPLIST_KEY_LSUIElement = YES; INFOPLIST_KEY_NSAppleEventsUsageDescription = "This app uses AppleEvents to control music"; INFOPLIST_KEY_NSCalendarsUsageDescription = "This app uses the calendar to display your calendar events"; INFOPLIST_KEY_NSCameraUsageDescription = "This app uses the camera to display a live camera view"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSRemindersUsageDescription = "This app uses Reminders to display your scheduled reminder in the calendar"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 2.7.3; PRODUCT_BUNDLE_IDENTIFIER = theboringteam.boringnotch; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_STRICT_CONCURRENCY = targeted; SWIFT_VERSION = 5.0; SYSTEM_FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(SDKROOT)/System/Library/PrivateFrameworks", ); }; name = Debug; }; 14CEF4232C5CAED400855D72 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES; CODE_SIGN_ENTITLEMENTS = boringNotch/boringNotch.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 271; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"boringNotch/Preview Content\""; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/mediaremote-adapter", ); GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = boringNotch/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TheBoringNotch; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSBackgroundOnly = NO; INFOPLIST_KEY_LSUIElement = YES; INFOPLIST_KEY_NSAppleEventsUsageDescription = "This app uses AppleEvents to control music"; INFOPLIST_KEY_NSCalendarsUsageDescription = "This app uses the calendar to display your calendar events"; INFOPLIST_KEY_NSCameraUsageDescription = "This app uses the camera to display a live camera view"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSRemindersUsageDescription = "This app uses Reminders to display your scheduled reminder in the calendar"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 2.7.3; PRODUCT_BUNDLE_IDENTIFIER = theboringteam.boringnotch; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_STRICT_CONCURRENCY = targeted; SWIFT_VERSION = 5.0; SYSTEM_FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(SDKROOT)/System/Library/PrivateFrameworks", ); }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 11F7485D2EC9AABA00F841DB /* Build configuration list for PBXNativeTarget "BoringNotchXPCHelper" */ = { isa = XCConfigurationList; buildConfigurations = ( 11F7485E2EC9AABA00F841DB /* Debug */, 11F7485F2EC9AABA00F841DB /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 14CEF40D2C5CAED200855D72 /* Build configuration list for PBXProject "boringNotch" */ = { isa = XCConfigurationList; buildConfigurations = ( 14CEF41F2C5CAED400855D72 /* Debug */, 14CEF4202C5CAED400855D72 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 14CEF4212C5CAED400855D72 /* Build configuration list for PBXNativeTarget "boringNotch" */ = { isa = XCConfigurationList; buildConfigurations = ( 14CEF4222C5CAED400855D72 /* Debug */, 14CEF4232C5CAED400855D72 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ 111BE95B2ECD71E10079DD4E /* XCRemoteSwiftPackageReference "AsyncXPCConnection" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/ChimeHQ/AsyncXPCConnection"; requirement = { kind = upToNextMajorVersion; minimumVersion = 1.3.0; }; }; 111BEA6D2ED166E20079DD4E /* XCRemoteSwiftPackageReference "MacroVisionKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/TheBoredTeam/MacroVisionKit"; requirement = { kind = upToNextMajorVersion; minimumVersion = 0.2.0; }; }; 1194E8832EA57D23009C82D6 /* XCRemoteSwiftPackageReference "SkyLightWindow" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Lakr233/SkyLightWindow"; requirement = { kind = upToNextMajorVersion; minimumVersion = 1.0.0; }; }; 11F748712EC9DA9300F841DB /* XCRemoteSwiftPackageReference "lottie-spm" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/airbnb/lottie-spm.git"; requirement = { kind = upToNextMajorVersion; minimumVersion = 4.5.2; }; }; 14D032182C68F32E0096E6A1 /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/sindresorhus/LaunchAtLogin-Modern"; requirement = { kind = upToNextMajorVersion; minimumVersion = 1.1.0; }; }; 14D0321B2C68F3350096E6A1 /* XCRemoteSwiftPackageReference "Sparkle" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/sparkle-project/Sparkle"; requirement = { kind = upToNextMajorVersion; minimumVersion = 2.8.0; }; }; 9A987A0E2C73CA8D005CA465 /* XCRemoteSwiftPackageReference "swift-collections" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/apple/swift-collections.git"; requirement = { kind = upToNextMajorVersion; minimumVersion = 1.1.2; }; }; 9A987A112C73CAA1005CA465 /* XCRemoteSwiftPackageReference "Pow" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/EmergeTools/Pow"; requirement = { kind = upToNextMajorVersion; minimumVersion = 1.0.4; }; }; B1628B902CC260C0003D8DF3 /* XCRemoteSwiftPackageReference "swiftui-introspect" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/siteline/swiftui-introspect"; requirement = { kind = upToNextMajorVersion; minimumVersion = 1.3.0; }; }; B18654372C6F4990000B926A /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/sindresorhus/KeyboardShortcuts"; requirement = { kind = upToNextMajorVersion; minimumVersion = 2.2.4; }; }; B19016202CC15B3D00E3F12E /* XCRemoteSwiftPackageReference "Defaults" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/sindresorhus/Defaults"; requirement = { kind = upToNextMajorVersion; minimumVersion = 9.0.2; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ 111BE95C2ECD71E10079DD4E /* AsyncXPCConnection */ = { isa = XCSwiftPackageProductDependency; package = 111BE95B2ECD71E10079DD4E /* XCRemoteSwiftPackageReference "AsyncXPCConnection" */; productName = AsyncXPCConnection; }; 111BEA502ECFBF7F0079DD4E /* MacroVisionKit */ = { isa = XCSwiftPackageProductDependency; productName = MacroVisionKit; }; 111BEA5E2ED07A340079DD4E /* MacroVisionKit */ = { isa = XCSwiftPackageProductDependency; productName = MacroVisionKit; }; 111BEA6E2ED166E20079DD4E /* MacroVisionKit */ = { isa = XCSwiftPackageProductDependency; package = 111BEA6D2ED166E20079DD4E /* XCRemoteSwiftPackageReference "MacroVisionKit" */; productName = MacroVisionKit; }; 1194E8842EA57D23009C82D6 /* SkyLightWindow */ = { isa = XCSwiftPackageProductDependency; package = 1194E8832EA57D23009C82D6 /* XCRemoteSwiftPackageReference "SkyLightWindow" */; productName = SkyLightWindow; }; 11F748722EC9DA9300F841DB /* Lottie */ = { isa = XCSwiftPackageProductDependency; package = 11F748712EC9DA9300F841DB /* XCRemoteSwiftPackageReference "lottie-spm" */; productName = Lottie; }; 14D032192C68F32E0096E6A1 /* LaunchAtLogin */ = { isa = XCSwiftPackageProductDependency; package = 14D032182C68F32E0096E6A1 /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */; productName = LaunchAtLogin; }; 14D0321C2C68F3350096E6A1 /* Sparkle */ = { isa = XCSwiftPackageProductDependency; package = 14D0321B2C68F3350096E6A1 /* XCRemoteSwiftPackageReference "Sparkle" */; productName = Sparkle; }; 9A987A0F2C73CA8D005CA465 /* Collections */ = { isa = XCSwiftPackageProductDependency; package = 9A987A0E2C73CA8D005CA465 /* XCRemoteSwiftPackageReference "swift-collections" */; productName = Collections; }; B1628B912CC260C0003D8DF3 /* SwiftUIIntrospect */ = { isa = XCSwiftPackageProductDependency; package = B1628B902CC260C0003D8DF3 /* XCRemoteSwiftPackageReference "swiftui-introspect" */; productName = SwiftUIIntrospect; }; B18654382C6F4990000B926A /* KeyboardShortcuts */ = { isa = XCSwiftPackageProductDependency; package = B18654372C6F4990000B926A /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */; productName = KeyboardShortcuts; }; B19016212CC15B3D00E3F12E /* Defaults */ = { isa = XCSwiftPackageProductDependency; package = B19016202CC15B3D00E3F12E /* XCRemoteSwiftPackageReference "Defaults" */; productName = Defaults; }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 14CEF40A2C5CAED200855D72 /* Project object */; } ================================================ FILE: boringNotch.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: boringNotch.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: boringNotch.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ ================================================ FILE: boringNotch.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved ================================================ { "originHash" : "ab961f5de25797b1a82bd0f8c39b561332a3dfe10de0a118e1252262fbd45864", "pins" : [ { "identity" : "asyncxpcconnection", "kind" : "remoteSourceControl", "location" : "https://github.com/ChimeHQ/AsyncXPCConnection", "state" : { "revision" : "da31dbcaa1b57949e46dcc19360b17d1a8de06bd", "version" : "1.3.0" } }, { "identity" : "defaults", "kind" : "remoteSourceControl", "location" : "https://github.com/sindresorhus/Defaults", "state" : { "revision" : "8192b0986611e105355a9433e99bf5c79469fbbd", "version" : "9.0.6" } }, { "identity" : "keyboardshortcuts", "kind" : "remoteSourceControl", "location" : "https://github.com/sindresorhus/KeyboardShortcuts", "state" : { "revision" : "1aef85578fdd4f9eaeeb8d53b7b4fc31bf08fe27", "version" : "2.4.0" } }, { "identity" : "launchatlogin-modern", "kind" : "remoteSourceControl", "location" : "https://github.com/sindresorhus/LaunchAtLogin-Modern", "state" : { "revision" : "a04ec1c363be3627734f6dad757d82f5d4fa8fcc", "version" : "1.1.0" } }, { "identity" : "lottie-spm", "kind" : "remoteSourceControl", "location" : "https://github.com/airbnb/lottie-spm.git", "state" : { "revision" : "04f2fd18cc9404a0a0917265a449002674f24ec9", "version" : "4.5.2" } }, { "identity" : "macrovisionkit", "kind" : "remoteSourceControl", "location" : "https://github.com/TheBoredTeam/MacroVisionKit", "state" : { "revision" : "da481a6be8d8b1bf7fcb218507a72428bbcae7b0", "version" : "0.2.0" } }, { "identity" : "pow", "kind" : "remoteSourceControl", "location" : "https://github.com/EmergeTools/Pow", "state" : { "revision" : "a504eb6d144bcf49f4f33029a2795345cb39e6b4", "version" : "1.0.5" } }, { "identity" : "skylightwindow", "kind" : "remoteSourceControl", "location" : "https://github.com/Lakr233/SkyLightWindow", "state" : { "revision" : "b7bd99f62a0673a99bed4bfd31098ca1dcdd10eb", "version" : "1.0.0" } }, { "identity" : "sparkle", "kind" : "remoteSourceControl", "location" : "https://github.com/sparkle-project/Sparkle", "state" : { "revision" : "9a1d2a19d3595fcf8d9c447173f9a1687b3dcadb", "version" : "2.8.0" } }, { "identity" : "swift-collections", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { "revision" : "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e", "version" : "1.3.0" } }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-syntax", "state" : { "revision" : "4799286537280063c85a32f09884cfbca301b1a1", "version" : "602.0.0" } }, { "identity" : "swiftui-introspect", "kind" : "remoteSourceControl", "location" : "https://github.com/siteline/swiftui-introspect", "state" : { "revision" : "807f73ce09a9b9723f12385e592b4e0aaebd3336", "version" : "1.3.0" } } ], "version" : 3 } ================================================ FILE: crowdin.yml ================================================ files: - source: /boringNotch/Localizable.xcstrings translation: /boringNotch/Localizable.xcstrings multilingual: 1 ================================================ FILE: mediaremote-adapter/MediaRemoteAdapter.framework/Versions/A/Resources/Info.plist ================================================ CFBundleDevelopmentRegion English CFBundleExecutable MediaRemoteAdapter CFBundleIconFile CFBundleIdentifier com.vandenbe.MediaRemoteAdapter CFBundleInfoDictionaryVersion 6.0 CFBundleName MediaRemoteAdapter CFBundlePackageType FMWK CFBundleSignature ???? CFBundleVersion 0.1.0 CFBundleShortVersionString 0.1 CSResourcesFileMapped ================================================ FILE: mediaremote-adapter/MediaRemoteAdapter.framework/Versions/A/_CodeSignature/CodeResources ================================================ files Resources/Info.plist M6AF1VWVJ1A/DSliCSjg170FqsY= files2 Resources/Info.plist hash2 z3yWmTAqjdrPJEZUQ+t6AVPhw0e/I8PAiVr0HIU2ivg= rules ^Resources/ ^Resources/.*\.lproj/ optional weight 1000 ^Resources/.*\.lproj/locversion.plist$ omit weight 1100 ^Resources/Base\.lproj/ weight 1010 ^version.plist$ rules2 .*\.dSYM($|/) weight 11 ^(.*/)?\.DS_Store$ omit weight 2000 ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/ nested weight 10 ^.* ^Info\.plist$ omit weight 20 ^PkgInfo$ omit weight 20 ^Resources/ weight 20 ^Resources/.*\.lproj/ optional weight 1000 ^Resources/.*\.lproj/locversion.plist$ omit weight 1100 ^Resources/Base\.lproj/ weight 1010 ^[^/]+$ nested weight 10 ^embedded\.provisionprofile$ weight 20 ^version\.plist$ weight 20 ================================================ FILE: mediaremote-adapter/mediaremote-adapter.pl ================================================ #!/usr/bin/perl # Copyright (c) 2025 Jonas van den Berg # This file is licensed under the BSD 3-Clause License. # For usage information read below or run the script without arguments. use strict; use warnings; use DynaLoader; use File::Spec; use File::Basename; sub print_help() { print <<'HELP'; Usage: mediaremote-adapter.pl FRAMEWORK_PATH [TEST_CLIENT_PATH] [FUNCTION [PARAMS|OPTIONS...]] FRAMEWORK_PATH: Absolute path to the MediaRemoteAdapter.framework directory TEST_CLIENT_PATH: (optional) Absolute path to the MediaRemoteAdapterTestClient executable. Only for "test" FUNCTION: stream Streams now playing information (as diff by default) get Prints now playing information once with all available metadata send Sends a command to the now playing application seek Seeks to a specific timeline position shuffle Sets the shuffle mode repeat Sets the repeat mode speed Sets the playback speed test Tests if the adapter is entitled to use the MediaRemote framework. An exit code other than 0 indicates the adapter is non-functional PARAMS: send(command) command: The MRCommand ID as a number (e.g. kMRPlay = 0) seek(position) position: The timeline position in microseconds shuffle(mode) mode: The shuffle mode repeat(mode) mode: The repeat mode speed(speed) speed: The playback speed OPTIONS: get --now: Adds an "elapsedTimeNow" key with an estimation of the current elapsed playback time. This estimation may be off by up to a second. To determine a more accurate time without polling "get" continuously, calculate it using the "elapsedTime" and "timestamp" keys. "elapsedTime" contains the elapsed time at the time that is stored in "timestamp". stream --no-diff: Disable diffing and always dump all metadata --debounce=N: Delay in milliseconds to prevent spam (0 by default) get, stream --micros: Replaces the following time keys with microsecond equivalents "duration" -> "durationMicros" "elapsedTime" -> "elapsedTimeMicros" "elapsedTimeNow" -> "elapsedTimeNowMicros" "timestamp" -> "timestampEpochMicros" (converted to epoch time) --human-readable, -h: Makes values human-readable. Use only for debugging. The JSON output is pretty-printed and the following keys are adapted: "artworkData" -> Binary data is truncated to a shorter representation Examples (script name and framework path omitted): stream --no-diff --debounce=100 send 2 # Toggles play/pause in the media player (kMRATogglePlayPause) repeat 3 # Sets the repeat mode to "playlist" (kMRARepeatModePlaylist) HELP exit 0; } if (!defined $ARGV[1]) { print_help(); } sub fail { my ($error) = @_; print STDERR "$error\n"; exit 1; } fail "Framework path not provided" unless @ARGV >= 1; my $framework_path = shift @ARGV; # Optionally accept MEDIAREMOTEADAPTER_TEST_CLIENT_PATH path as second argument my $maybe_helper_path = $ARGV[0] // ''; if ($maybe_helper_path =~ m{/}){ my $helper_path = shift @ARGV; $ENV{MEDIAREMOTEADAPTER_TEST_CLIENT_PATH} = $helper_path; } if (!defined $ARGV[0]) { print_help(); } my $framework_basename = File::Basename::basename($framework_path); fail "Provided path is not a framework: $framework_path" unless $framework_basename =~ s/\.framework$//; my $framework = File::Spec->catfile($framework_path, $framework_basename); fail "Framework not found at $framework" unless -e $framework; my $handle = DynaLoader::dl_load_file($framework, 0) or fail "Failed to load framework: $framework"; my $function_name = shift @ARGV or fail "Missing function name"; fail "Invalid function name: '$function_name'" unless $function_name eq "stream" || $function_name eq "get" || $function_name eq "send" || $function_name eq "seek" || $function_name eq "shuffle" || $function_name eq "repeat" || $function_name eq "speed" || $function_name eq "test"; sub parse_options { my ($start_index) = @_; my %arg_map; my $i = $start_index; while ($i <= $#ARGV) { my $arg = $ARGV[$i]; if ($arg =~ /^--([a-z\\-]+)(?:=(.*))?$/) { my $key = $1; my $value = defined $2 ? $2 : undef; $arg_map{$key} = $value; splice @ARGV, $i, 1; } elsif ($arg =~ /^-([a-zA-Z]+)$/) { my @flags = split //, $1; $arg_map{$_} = undef for @flags; splice @ARGV, $i, 1; } else { $i++; } } return \%arg_map; } sub env_func { my $symbol_name = shift; return "${symbol_name}_env"; } sub set_env_param { my ($func, $index, $name, $value) = @_; $ENV{"MEDIAREMOTEADAPTER_PARAM_${func}_${index}_${name}"} = "$value"; } sub set_env_option_unsafe { my ($name, $value) = @_; $name =~ s/-/_/g; $ENV{"MEDIAREMOTEADAPTER_OPTION_${name}"} = defined $value ? "$value" : ""; } sub set_env_option { my ($options, $key) = @_; my $value = $options->{$key}; if (defined $value) { fail "Unexpected value for option '$key'"; } set_env_option_unsafe($key, $value); } sub set_env_option_value { my ($options, $key) = @_; my $value = $options->{$key}; if (!defined $value) { fail "Missing value for option '$key'"; } set_env_option_unsafe($key, $value); } my $symbol_name = "adapter_$function_name"; if ($function_name eq "send") { my $id = shift @ARGV; fail "Missing ID for '$function_name' command" unless defined $id; set_env_param($symbol_name, 0, "command", "$id"); $symbol_name = env_func($symbol_name); } elsif ($function_name eq "stream") { my $options = parse_options(0); foreach my $key (keys %{$options}) { if ($key eq "no-diff") { set_env_option($options, $key); } elsif ($key eq "debounce") { set_env_option_value($options, $key); } elsif ($key eq "micros") { set_env_option($options, $key); } elsif ($key eq "human-readable" || $key eq "h") { set_env_option($options, "human-readable"); } else { fail "Unrecognized option '$key'"; } } $symbol_name = env_func($symbol_name); } elsif ($function_name eq "get") { my $options = parse_options(0); foreach my $key (keys %{$options}) { if ($key eq "micros") { set_env_option($options, $key); } elsif ($key eq "human-readable" || $key eq "h") { set_env_option($options, "human-readable"); } elsif ($key eq "now") { set_env_option($options, $key); } else { fail "Unrecognized option '$key'"; } } $symbol_name = env_func($symbol_name); } elsif ($function_name eq "seek") { my $position = shift @ARGV; fail "Missing position for '$function_name' command" unless defined $position; set_env_param($symbol_name, 0, "position", "$position"); $symbol_name = env_func($symbol_name); } elsif ($function_name eq "shuffle") { my $mode = shift @ARGV; fail "Missing mode for '$function_name' command" unless defined $mode; set_env_param($symbol_name, 0, "mode", "$mode"); $symbol_name = env_func($symbol_name); } elsif ($function_name eq "repeat") { my $mode = shift @ARGV; fail "Missing mode for '$function_name' command" unless defined $mode; set_env_param($symbol_name, 0, "mode", "$mode"); $symbol_name = env_func($symbol_name); } elsif ($function_name eq "speed") { my $speed = shift @ARGV; fail "Missing speed for '$function_name' command" unless defined $speed; set_env_param($symbol_name, 0, "speed", "$speed"); $symbol_name = env_func($symbol_name); } elsif ($function_name eq "test") { $symbol_name = "adapter_test"; } if (defined shift @ARGV) { fail "Too many arguments"; } my $symbol = DynaLoader::dl_find_symbol($handle, "$symbol_name") or fail "Symbol '$symbol_name' not found in $framework"; DynaLoader::dl_install_xsub("main::$function_name", $symbol); eval { no strict "refs"; &{"main::$function_name"}(); }; if ($@) { fail "Error executing $function_name: $@"; } ================================================ FILE: updater/appcast.xml ================================================ 2.7.3 Mon, 24 Nov 2025 08:07:37 +0000 https://github.com/TheBoredTeam/boring.notch/releases 271 2.7.3 14.0 🚀 v2.7.3— Flying Rabbit 🐇🪽

Fixes:

  • Fixed regression in album artwork view
  • Fixed HUD for older versions of macOS
  • Added volume feedback for HUD when enabled in macOS settings
  • Added option to display percentages for HUDs
  • Created a new custom HUD for when the notch is open
  • Fixed NowPlaying controller launching Apple Music by itself
  • Improved responsiveness of volume slider
  • Improved onboarding experience

🚀 v2.7 — Flying Rabbit 🐇🪽

Shelf 2.0

Major update with improved stability, enhanced functionality, and a refreshed UI.

  • New Context Menu – Right-click in the notch to access various file actions
  • Multi-Item Selection – Hold for consecutive or for non-consecutive selections
  • Double-Click to Open – Double-click on selected files to open them
  • Move by Default – Dragging files now moves them; hold to copy
  • Simplified Removal – Files can be removed after dragging (configurable in Settings)
  • Expanded Drag Detection – The shelf opens when files are dragged into the notch area
  • Expanded Sharing – More sharing services available in Settings

Complete HUD Replacement

Full support for macOS system controls:

  • Screen Brightness, Keyboard Brightness, and System Volume
  • Keyboard Brightness Controls: ⌘ + Brightness Down/Up
  • Option () – Triggers alternate action configured in Settings
  • Option + Shift ( ) – Adjustments in smaller increments

Music Enhancements

  • YouTube Music Support Rewritten – Now powered by WebSocket for better accuracy and performance

Note: Requires version 3.11+ of YouTube Music/Pear Desktop. Updating is strongly recommended.

  • Redesigned Music Controls – Fully customizable:
    • Skip backward/forward by 15 seconds
    • Adjust music app volume
    • Mark favorite songs (Apple Music & YouTube MusiZ
    • Rearrange or remove existing controls
  • Optimized Spotify Artwork Cache – Significantly reduced storage usage with automatic cleanup
  • Lyrics (Beta) – View synchronized lyrics for currently playing songs

Calendar Improvements

  • New setting to hide all-day events
  • New setting to auto-scroll to next current or upcoming event
  • New setting to prevent truncation of long event names

Improved Window Behavior

  • Enhanced Fullscreen Detection – Significantly more reliable
  • Better Edge Handling – Fixed top edge cursor issues
  • Reduced Title Bar Interference – Less intrusive during fullscreen
  • Lock Screen Support – Notch now appears on lock screen
  • Screenshot Privacy – Hide notch from screenshots and recordings

Advanced Settings

  • Cleaner interface with lesser-used settings moved to Advanced
  • Accent color override reintroduced

General Enhancements

  • Numerous UI fixes and polish
  • Localization updates across all supported languages

👋 New Contributors

@azhao4227 · @bueckerlars · @TheMalenia · @Corentin132 · @SupKittyMeow · @M7T5M3P · @charshith · @Decryptu · @EnesCinr · @lambegraham


📄 Full Changelog

Compare v2.7-rc.3 → v2.7

]]>
2.7.2 Sat, 22 Nov 2025 22:40:29 +0000 https://github.com/TheBoredTeam/boring.notch/releases 262 2.7.2 14.0 🚀 v2.7.2— Flying Rabbit 🐇🪽

Fixes:

  • Fixed default sneak peak

🚀 v2.7.1 — Flying Rabbit 🐇🪽

Fixes:

🚀 v2.7 — Flying Rabbit 🐇🪽

Shelf 2.0

Major update with improved stability, enhanced functionality, and a refreshed UI.

  • New Context Menu – Right-click in the notch to access various file actions
  • Multi-Item Selection – Hold for consecutive or for non-consecutive selections
  • Double-Click to Open – Double-click on selected files to open them
  • Move by Default – Dragging files now moves them; hold to copy
  • Simplified Removal – Files can be removed after dragging (configurable in Settings)
  • Expanded Drag Detection – The shelf opens when files are dragged into the notch area
  • Expanded Sharing – More sharing services available in Settings

Complete HUD Replacement

Full support for macOS system controls:

  • Screen Brightness, Keyboard Brightness, and System Volume
  • Keyboard Brightness Controls: ⌘ + Brightness Down/Up
  • Option () – Triggers alternate action configured in Settings
  • Option + Shift ( ) – Adjustments in smaller increments

Music Enhancements

  • YouTube Music Support Rewritten – Now powered by WebSocket for better accuracy and performance

Note: Requires version 3.11+ of YouTube Music/Pear Desktop. Updating is strongly recommended.

  • Redesigned Music Controls – Fully customizable:
    • Skip backward/forward by 15 seconds
    • Adjust music app volume
    • Mark favorite songs (Apple Music & YouTube MusiZ
    • Rearrange or remove existing controls
  • Optimized Spotify Artwork Cache – Significantly reduced storage usage with automatic cleanup
  • Lyrics (Beta) – View synchronized lyrics for currently playing songs

Calendar Improvements

  • New setting to hide all-day events
  • New setting to auto-scroll to next current or upcoming event
  • New setting to prevent truncation of long event names

Improved Window Behavior

  • Enhanced Fullscreen Detection – Significantly more reliable
  • Better Edge Handling – Fixed top edge cursor issues
  • Reduced Title Bar Interference – Less intrusive during fullscreen
  • Lock Screen Support – Notch now appears on lock screen
  • Screenshot Privacy – Hide notch from screenshots and recordings

Advanced Settings

  • Cleaner interface with lesser-used settings moved to Advanced
  • Accent color override reintroduced

General Enhancements

  • Numerous UI fixes and polish
  • Localization updates across all supported languages

👋 New Contributors

@azhao4227 · @bueckerlars · @TheMalenia · @Corentin132 · @SupKittyMeow · @M7T5M3P · @charshith · @Decryptu · @EnesCinr · @lambegraham


📄 Full Changelog

Compare v2.7-rc.3 → v2.7

]]>
2.7.1 Sat, 22 Nov 2025 22:14:41 +0000 https://github.com/TheBoredTeam/boring.notch/releases 260 2.7.1 14.0 🚀 v2.7.1 — Flying Rabbit 🐇🪽

Fixes:

🚀 v2.7 — Flying Rabbit 🐇🪽

Shelf 2.0

Major update with improved stability, enhanced functionality, and a refreshed UI.

  • New Context Menu – Right-click in the notch to access various file actions
  • Multi-Item Selection – Hold for consecutive or for non-consecutive selections
  • Double-Click to Open – Double-click on selected files to open them
  • Move by Default – Dragging files now moves them; hold to copy
  • Simplified Removal – Files can be removed after dragging (configurable in Settings)
  • Expanded Drag Detection – The shelf opens when files are dragged into the notch area
  • Expanded Sharing – More sharing services available in Settings

Complete HUD Replacement

Full support for macOS system controls:

  • Screen Brightness, Keyboard Brightness, and System Volume
  • Keyboard Brightness Controls: ⌘ + Brightness Down/Up
  • Option () – Triggers alternate action configured in Settings
  • Option + Shift ( ) – Adjustments in smaller increments

Music Enhancements

  • YouTube Music Support Rewritten – Now powered by WebSocket for better accuracy and performance

Note: Requires version 3.11+ of YouTube Music/Pear Desktop. Updating is strongly recommended.

  • Redesigned Music Controls – Fully customizable:
    • Skip backward/forward by 15 seconds
    • Adjust music app volume
    • Mark favorite songs (Apple Music & YouTube MusiZ
    • Rearrange or remove existing controls
  • Optimized Spotify Artwork Cache – Significantly reduced storage usage with automatic cleanup
  • Lyrics (Beta) – View synchronized lyrics for currently playing songs

Calendar Improvements

  • New setting to hide all-day events
  • New setting to auto-scroll to next current or upcoming event
  • New setting to prevent truncation of long event names

Improved Window Behavior

  • Enhanced Fullscreen Detection – Significantly more reliable
  • Better Edge Handling – Fixed top edge cursor issues
  • Reduced Title Bar Interference – Less intrusive during fullscreen
  • Lock Screen Support – Notch now appears on lock screen
  • Screenshot Privacy – Hide notch from screenshots and recordings

Advanced Settings

  • Cleaner interface with lesser-used settings moved to Advanced
  • Accent color override reintroduced

General Enhancements

  • Numerous UI fixes and polish
  • Localization updates across all supported languages

👋 New Contributors

@azhao4227 · @bueckerlars · @TheMalenia · @Corentin132 · @SupKittyMeow · @M7T5M3P · @charshith · @Decryptu · @EnesCinr · @lambegraham


📄 Full Changelog

Compare v2.7-rc.3 → v2.7

]]>
2.7-rc.1 Flying Rabbit 🐇🪽 Sun, 27 Jul 2025 09:41:40 +0530 2.7-rc.1+3 2.7-rc.1 14.2

🐇 boring.notch v2.7 – Flying Rabbit RC 1

Release Candidate - July 27, 2025

✨ What's New & Improved

  • 🛠️ Fixed hanging issues: Resolved stability concerns by addressing test instability. (by @Alexander5015)
  • 📅 Calendar Settings Resolved: Calendar settings now update correctly with authorization; proper show/hide.
  • 🎵 YouTube Music Controller:
    • Improved shuffle and repeat UX, beta features for shuffle/repeat now available.
    • Fixed seek control bugs and added forced polling.
    (by @pranav1st & @Alexander5015)
  • 🔲 MediaRemoteAdapter.Framework updated
  • 🔃 Button order and repeat toggle improved: Better player button logic and new repeat toggle.
  • 🐞 Now Playing Controller Beta: Beta enhancements and settings (known bugs remain for testing).
  • ♻️ Shuffle is now always enabled.
  • 💡 Refactored Shuffle & Repeat: toggleShuffle/toggleRepeat refactored for improved experience.

— The Boring Team

]]>
2.7-rc.0 Flying Rabbit 🐇🪽 Sat, 26 Jul 2025 12:45:19 +0530 2.7-rc.0+1 2.7-rc.0 14.2 🎉 Release v2.7 Flying Rabbit RC 0 — Boring Notch

We're thrilled to announce Boring Notch v2.7: Flying Rabbit RC 0, packed with powerful new features, polish, and community contributions!

🚀 Highlights

  • 🔋 Enhanced Battery Status \& Charging Experience Thanks to @AlexLemus-Dev
    • More informative battery menu: percentage, max capacity, charging, low power, and status icons
    • Configurable battery indicators, notifications, and display options
    • Interactive battery icon with detailed info and instant System Preferences access
    • Visual alignment and consistent styling (macOS-like bolt/plug icons, dark mode, etc.)
    • Optimized and documented code, robust error handling, and improved performance
    • Details \& Demo
  • 🖥️ Fullscreen Detection \& Playback Management Fixes @Alexander5015 improved reliability of media controls during fullscreen transitions
  • 🦷 Allow 0 Height Notch Now supports notches with zero height for enhanced layout flexibility
  • 🎛️ Multiple Media Controllers Control and display several media controllers at once
  • 👀 Sneak Peek \& Jiggle Fixes Re-added sneak peek and improved animation stability
  • 🗓️ New Calendar Service Seamlessly integrates your calendar into Boring Notch
  • 📄 Updated LICENSE Keeping compliance and clarity up to date
  • 🆕 Onboarding \& Better Settings Window Streamlined onboarding and redesigned settings for easy configuration
  • 📸 Camera Toggle Feature New camera quick toggle, privacy-first!
  • 🎵 MediaRemote Adapter Support Improved compatibility with MediaRemote devices

🆕 Welcoming First-Time Contributors!

Contributor PR Link
@sancho1952007 (https://github.com/TheBoredTeam/boring.notch/pull/431)
@Ein-Tim (https://github.com/TheBoredTeam/boring.notch/pull/405)
@AlexLemus-Dev (https://github.com/TheBoredTeam/boring.notch/pull/437)
@yaxarat (https://github.com/TheBoredTeam/boring.notch/pull/397)
@ShirakawaMio (https://github.com/TheBoredTeam/boring.notch/pull/399)
@divyanshu0469 (https://github.com/TheBoredTeam/boring.notch/pull/454)
@oorischubert (https://github.com/TheBoredTeam/boring.notch/pull/409)
@Al3Gr (https://github.com/TheBoredTeam/boring.notch/pull/493)
@ChemicalChaos-Fabian42 (https://github.com/TheBoredTeam/boring.notch/pull/509)
@Davetheword (https://github.com/TheBoredTeam/boring.notch/pull/501)
@Steve-sy (https://github.com/TheBoredTeam/boring.notch/pull/598)

🛠️ Other Improvements

  • Visual and animation refinements for battery/media indicators
  • Enhanced error handling, code reorganization, and extensive documentation
  • Optimizations for consistent performance across all system conditions

Thank you 🌟 to everyone—new and returning—for their effort in making this release feature-rich, steady, and super fun!

Try Boring Notch v2.7 — Flying Rabbit RC 0 and let us know what you think!

— The Boring Notch Team

]]>
2.6 🎉 Wolf Painting 🐺 Sun, 23 Feb 2025 22:02:47 +0530 2.6+1 2.6 14.2 🎉 v2.6 🚀 Wolf Painting 🐺

We're thrilled to announce the release of Wolf Painting version 2.6! 🎊 This major update brings numerous improvements and new features that will make your experience even more enjoyable.


Download from here

🤔 What's Changed? 🤓

  • 🧠 Improved memory management and thread safety across the app
  • 🎥 Enhanced webcam handling with better session lifecycle management
  • 🎨 Added option to choose between classic and modern notch animations
  • ⚡️ Improved fullscreen detection with Finder exclusion
  • 🔄 Better hover state handling and reduced animation flicker
  • 🔋 Enhanced power status notifications and battery indicators
  • ⚙️ Improved screen lock and display change handling
  • 🎵 Better music playback state tracking and elapsed time accuracy
  • ✨ Added "Buy us a coffee" option in Welcome screen
  • ⚠️ Added warning badges for unsupported extensions
  • 🐛 Various bug fixes and stability improvements
  • For more details, check out our website
]]>