├── .claude └── settings.local.json ├── .github └── workflows │ ├── build-and-release.yml │ └── swift.yml ├── .gitignore ├── .periphery.yml ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── NativeYoutube.xcodeproj ├── project.pbxproj ├── project.pbxproj.backup ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcuserdata │ │ ├── aayush29.xcuserdatad │ │ └── UserInterfaceState.xcuserstate │ │ └── aayushpokharel.xcuserdatad │ │ └── UserInterfaceState.xcuserstate ├── xcshareddata │ └── xcschemes │ │ └── NativeYoutube.xcscheme └── xcuserdata │ ├── aayush29.xcuserdatad │ └── xcschemes │ │ └── xcschememanagement.plist │ └── aayushpokharel.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist ├── NativeYoutube ├── AppCoordinator.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── AppIcon 1.png │ │ ├── AppIcon 2.png │ │ ├── AppIcon 3.png │ │ ├── AppIcon 4.png │ │ ├── AppIcon 5.png │ │ ├── AppIcon 6.png │ │ ├── AppIcon 7.png │ │ ├── AppIcon 8.png │ │ ├── AppIcon 9.png │ │ ├── AppIcon.png │ │ └── Contents.json │ ├── AppIconImage.imageset │ │ ├── AppIcon 2.png │ │ ├── AppIcon 3.png │ │ ├── AppIcon 5.png │ │ └── Contents.json │ └── Contents.json ├── Clients │ ├── AppStateClient.swift │ ├── WindowClient │ │ ├── FloatingPanel.swift │ │ └── FloatingWindowClient.swift │ └── YouTubeKitClient.swift ├── ContentView.swift ├── Info.plist ├── NativeYoutube.entitlements ├── NativeYoutubeApp.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Sharing+Keys.swift └── Views │ ├── PlayListView │ └── PlayListView.swift │ ├── PreferencesView │ ├── GeneralPreferenceView.swift │ ├── LogPrefrenceView.swift │ ├── PreferencesView.swift │ └── YoutubePreferenceView.swift │ ├── SearchView │ └── SearchVideosView.swift │ └── YouTubePlayerView.swift ├── NativeYoutubeKit ├── .gitignore ├── Package.swift └── Sources │ ├── APIClient │ └── APIClient.swift │ ├── Assets │ ├── Assets.swift │ └── Resources │ │ └── Assets.xcassets │ │ ├── AppIconImage.imageset │ │ ├── AppIcon 2.png │ │ ├── AppIcon 3.png │ │ ├── AppIcon 5.png │ │ └── Contents.json │ │ └── Contents.json │ ├── Clients │ ├── PlaylistClient.swift │ └── SearchClient.swift │ ├── Models │ ├── Pages.swift │ ├── Video.swift │ ├── VideoClickBehaviour.swift │ └── YouTube │ │ ├── PlaylistResponse.swift │ │ ├── SearchResponse.swift │ │ ├── SharedModels.swift │ │ └── VideoTransformers.swift │ ├── NativeYoutubeKit │ └── NativeYoutubeKit.swift │ ├── Shared │ ├── DateConverter.swift │ └── Shared.swift │ └── UI │ ├── Color+Hexstring.swift │ ├── Components │ ├── BottomBarView.swift │ ├── CleanButton.swift │ ├── VideoContextMenuView.swift │ ├── VideoListView.swift │ ├── VideoRowView.swift │ └── WelcomeView.swift │ ├── GlowEffect.swift │ ├── GlowEffectViewModifier.swift │ └── ViewModifiers │ ├── ThinBackground.swift │ └── VisualEffectView.swift ├── README.md └── sparkle ├── README.md └── appcast.xml /.claude/settings.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissions": { 3 | "allow": [ 4 | "Bash(grep:*)", 5 | "Bash(rm:*)", 6 | "Bash(git:*)", 7 | "Bash(ls:*)", 8 | "Bash(cat:*)", 9 | "Bash(awk:*)", 10 | "Bash(mv:*)", 11 | "Bash(cp:*)", 12 | "Bash(ls:*)", 13 | "Bash(find:*)", 14 | "Bash(test:*)", 15 | "Bash(mkdir:*)", 16 | "Bash(touch:*)", 17 | "Bash(cp:*)", 18 | "Bash(xcodebuild:*)", 19 | "Bash(swift package:*)", 20 | "Bash(swift test)", 21 | "Bash(chmod:*)", 22 | "Bash(swift:*)", 23 | "Bash(rg:*)" 24 | ], 25 | "deny": [] 26 | } 27 | } -------------------------------------------------------------------------------- /.github/workflows/build-and-release.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release 2 | 3 | on: 4 | release: 5 | types: [created] 6 | workflow_dispatch: # Allow manual triggering 7 | 8 | jobs: 9 | build: 10 | runs-on: macos-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up Xcode 17 | uses: maxim-lobanov/setup-xcode@v1 18 | with: 19 | xcode-version: latest-stable 20 | 21 | - name: Configure Xcode defaults 22 | run: | 23 | set -e 24 | # Check if the defaults exist before trying to delete them 25 | defaults read com.apple.dt.Xcode IDEPackageOnlyUseVersionsFromResolvedFile 2>/dev/null && defaults delete com.apple.dt.Xcode IDEPackageOnlyUseVersionsFromResolvedFile || true 26 | defaults read com.apple.dt.Xcode IDEDisableAutomaticPackageResolution 2>/dev/null && defaults delete com.apple.dt.Xcode IDEDisableAutomaticPackageResolution || true 27 | defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES 28 | 29 | - name: Install dependencies 30 | run: | 31 | brew install coreutils 32 | 33 | - name: Import Code Signing Certificate 34 | env: 35 | SIGNING_CERTIFICATE_BASE64: ${{ secrets.SIGNING_CERTIFICATE_BASE64 }} 36 | SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.SIGNING_CERTIFICATE_PASSWORD }} 37 | DEVELOPMENT_TEAM: ${{ secrets.DEVELOPMENT_TEAM }} 38 | run: | 39 | # Create variables 40 | CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 41 | KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db 42 | KEYCHAIN_PASSWORD=temp_password_$(date +%s) 43 | 44 | # Import certificate from secrets 45 | echo "$SIGNING_CERTIFICATE_BASE64" | base64 --decode > "$CERTIFICATE_PATH" 46 | 47 | # Create temporary keychain 48 | security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" 49 | security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" 50 | security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" 51 | 52 | # Import certificate to keychain 53 | security import "$CERTIFICATE_PATH" -P "$SIGNING_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" 54 | security list-keychain -d user -s "$KEYCHAIN_PATH" 55 | security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" 56 | 57 | - name: Build and Archive 58 | env: 59 | DEVELOPMENT_TEAM: ${{ secrets.DEVELOPMENT_TEAM }} 60 | run: | 61 | xcodebuild -project NativeYoutube.xcodeproj \ 62 | -scheme NativeYoutube \ 63 | -configuration Release \ 64 | -archivePath $RUNNER_TEMP/NativeYoutube.xcarchive \ 65 | -allowProvisioningUpdates \ 66 | DEVELOPMENT_TEAM="$DEVELOPMENT_TEAM" \ 67 | CODE_SIGN_STYLE=Automatic \ 68 | archive 69 | 70 | - name: Export Archive 71 | env: 72 | DEVELOPMENT_TEAM: ${{ secrets.DEVELOPMENT_TEAM }} 73 | run: | 74 | # Create export options plist 75 | cat > $RUNNER_TEMP/ExportOptions.plist < 77 | 78 | 79 | 80 | method 81 | developer-id 82 | teamID 83 | $DEVELOPMENT_TEAM 84 | signingStyle 85 | automatic 86 | 87 | 88 | EOF 89 | 90 | xcodebuild -exportArchive \ 91 | -archivePath $RUNNER_TEMP/NativeYoutube.xcarchive \ 92 | -exportOptionsPlist $RUNNER_TEMP/ExportOptions.plist \ 93 | -exportPath $RUNNER_TEMP/export \ 94 | -allowProvisioningUpdates 95 | 96 | - name: Notarize Application 97 | env: 98 | APPLE_ID: ${{ secrets.APPLE_ID }} 99 | APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} 100 | DEVELOPMENT_TEAM: ${{ secrets.DEVELOPMENT_TEAM }} 101 | run: | 102 | # Create ZIP for notarization 103 | if [[ "${{ github.event_name }}" == "release" ]]; then 104 | VERSION=${{ github.event.release.tag_name }} 105 | VERSION=${VERSION#v} 106 | else 107 | VERSION=${GITHUB_REF_NAME#v} 108 | fi 109 | APP_PATH="$RUNNER_TEMP/export/NativeYoutube.app" 110 | ZIP_PATH="$RUNNER_TEMP/NativeYoutube-$VERSION-notarize.zip" 111 | 112 | ditto -c -k --keepParent "$APP_PATH" "$ZIP_PATH" 113 | 114 | # Submit for notarization 115 | xcrun notarytool submit "$ZIP_PATH" \ 116 | --apple-id "$APPLE_ID" \ 117 | --password "$APPLE_ID_PASSWORD" \ 118 | --team-id "$DEVELOPMENT_TEAM" \ 119 | --wait 120 | 121 | # Staple the notarization ticket 122 | xcrun stapler staple "$APP_PATH" 123 | 124 | - name: Create Release Assets 125 | run: | 126 | if [[ "${{ github.event_name }}" == "release" ]]; then 127 | VERSION=${{ github.event.release.tag_name }} 128 | VERSION=${VERSION#v} 129 | else 130 | VERSION=${GITHUB_REF_NAME#v} 131 | fi 132 | APP_PATH="$RUNNER_TEMP/export/NativeYoutube.app" 133 | 134 | # Create ZIP for distribution 135 | ditto -c -k --keepParent "$APP_PATH" "NativeYoutube-$VERSION.zip" 136 | 137 | # Create DMG 138 | brew install create-dmg 139 | create-dmg \ 140 | --volname "Native Youtube" \ 141 | --window-pos 200 120 \ 142 | --window-size 600 400 \ 143 | --icon-size 100 \ 144 | --icon "NativeYoutube.app" 200 200 \ 145 | --hide-extension "NativeYoutube.app" \ 146 | --app-drop-link 400 200 \ 147 | "NativeYoutube-$VERSION.dmg" \ 148 | "$APP_PATH" 149 | 150 | - name: Sign Sparkle Update 151 | env: 152 | SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }} 153 | run: | 154 | if [[ "${{ github.event_name }}" == "release" ]]; then 155 | VERSION=${{ github.event.release.tag_name }} 156 | VERSION=${VERSION#v} 157 | else 158 | VERSION=${GITHUB_REF_NAME#v} 159 | fi 160 | 161 | # Download Sparkle tools 162 | curl -L https://github.com/sparkle-project/Sparkle/releases/latest/download/Sparkle-2.x.tar.xz -o sparkle.tar.xz 163 | tar -xf sparkle.tar.xz 164 | 165 | # Create private key file from secret 166 | echo "$SPARKLE_PRIVATE_KEY" | base64 --decode > sparkle_private_key.pem 167 | 168 | # Sign the update 169 | ./bin/sign_update "NativeYoutube-$VERSION.zip" -f sparkle_private_key.pem 170 | 171 | # Get signature for appcast 172 | SIGNATURE=$(./bin/sign_update "NativeYoutube-$VERSION.zip" -f sparkle_private_key.pem | awk '/sparkle:edSignature=/ {print $2}' | tr -d '"') 173 | echo "SPARKLE_SIGNATURE=$SIGNATURE" >> $GITHUB_ENV 174 | 175 | # Clean up 176 | rm sparkle_private_key.pem 177 | 178 | - name: Generate Appcast Entry 179 | run: | 180 | if [[ "${{ github.event_name }}" == "release" ]]; then 181 | VERSION=${{ github.event.release.tag_name }} 182 | VERSION=${VERSION#v} 183 | else 184 | VERSION=${GITHUB_REF_NAME#v} 185 | fi 186 | FILE_SIZE=$(stat -f%z "NativeYoutube-$VERSION.zip") 187 | PUB_DATE=$(date -u +"%a, %d %b %Y %H:%M:%S +0000") 188 | BUILD_NUMBER=$(defaults read "$RUNNER_TEMP/export/NativeYoutube.app/Contents/Info.plist" CFBundleVersion) 189 | 190 | cat > appcast-item.xml << EOF 191 | 192 | Version $VERSION 193 | Version $VERSION 195 |

See the release notes for details.

196 | ]]>
197 | $PUB_DATE 198 | $BUILD_NUMBER 199 | $VERSION 200 | 14.0 201 | 205 |
206 | EOF 207 | 208 | - name: Upload Release Assets 209 | uses: softprops/action-gh-release@v1 210 | with: 211 | files: | 212 | NativeYoutube-*.dmg 213 | NativeYoutube-*.zip 214 | appcast-item.xml 215 | tag_name: ${{ github.event.release.tag_name }} 216 | env: 217 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 218 | 219 | - name: Update Appcast 220 | run: | 221 | # Clone the repository 222 | git config user.name "GitHub Actions" 223 | git config user.email "actions@github.com" 224 | 225 | # Checkout main branch 226 | git fetch origin main 227 | git checkout main 228 | 229 | # Update appcast.xml 230 | if [ ! -f sparkle/appcast.xml ]; then 231 | echo '' > sparkle/appcast.xml 232 | echo '' >> sparkle/appcast.xml 233 | echo ' ' >> sparkle/appcast.xml 234 | echo ' Native Youtube' >> sparkle/appcast.xml 235 | echo ' ' >> sparkle/appcast.xml 236 | echo '' >> sparkle/appcast.xml 237 | fi 238 | 239 | # Insert new item into appcast 240 | sed -i '' '/<\/channel>/i\ 241 | '"$(cat appcast-item.xml | sed 's/$/\\/')" sparkle/appcast.xml 242 | 243 | # Commit and push 244 | git add sparkle/appcast.xml 245 | 246 | if [[ "${{ github.event_name }}" == "release" ]]; then 247 | VERSION=${{ github.event.release.tag_name }} 248 | VERSION=${VERSION#v} 249 | else 250 | VERSION=${GITHUB_REF_NAME#v} 251 | fi 252 | 253 | git commit -m "Update appcast for version ${VERSION}" 254 | git push origin main 255 | env: 256 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Swift 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set up Xcode 17 | uses: maxim-lobanov/setup-xcode@v1 18 | with: 19 | xcode-version: '16.1' 20 | - name: Configure Xcode defaults 21 | run: | 22 | set -e 23 | # Check if the defaults exist before trying to delete them 24 | defaults read com.apple.dt.Xcode IDEPackageOnlyUseVersionsFromResolvedFile 2>/dev/null && defaults delete com.apple.dt.Xcode IDEPackageOnlyUseVersionsFromResolvedFile || true 25 | defaults read com.apple.dt.Xcode IDEDisableAutomaticPackageResolution 2>/dev/null && defaults delete com.apple.dt.Xcode IDEDisableAutomaticPackageResolution || true 26 | defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES 27 | - name: Build 28 | run: xcodebuild -scheme NativeYoutube -destination 'platform=macOS' build 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.playground 3 | .SwiftPM 4 | .build/ 5 | .claude/ 6 | *.xcodeproj/xcuserdata/ 7 | *.xcworkspace/xcuserdata/ 8 | *.xcuserstate 9 | xcshareddata/ 10 | DerivedData/ 11 | *.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 12 | .swiftpm/ 13 | .vendor/ 14 | 15 | # Environment files - NEVER commit these! 16 | .env 17 | 18 | # Sparkle keys - NEVER commit these! 19 | sparkle/dsa_priv.pem 20 | sparkle-keys/ 21 | 22 | # Build artifacts 23 | build/ 24 | dist/ 25 | 26 | # Certificates 27 | *.p12 28 | *.cer 29 | -------------------------------------------------------------------------------- /.periphery.yml: -------------------------------------------------------------------------------- 1 | clean_build: true 2 | project: NativeYoutube.xcodeproj 3 | retain_assign_only_properties: true 4 | schemes: 5 | - NativeYoutube 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Aayush Pokharel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NativeYoutube.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 70; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 512B281A2DD8F8D000D72AB7 /* Clients in Frameworks */ = {isa = PBXBuildFile; productRef = 5170B1662DD8F11100734BCF /* Clients */; }; 11 | 513960D22DD8D7F5003E13D9 /* APIClient in Frameworks */ = {isa = PBXBuildFile; productRef = 513960D12DD8D7F5003E13D9 /* APIClient */; }; 12 | 513960D42DD8D7F5003E13D9 /* Assets in Frameworks */ = {isa = PBXBuildFile; productRef = 513960D32DD8D7F5003E13D9 /* Assets */; }; 13 | 513960D62DD8D7F5003E13D9 /* Models in Frameworks */ = {isa = PBXBuildFile; productRef = 513960D52DD8D7F5003E13D9 /* Models */; }; 14 | 513960D82DD8D7F5003E13D9 /* Shared in Frameworks */ = {isa = PBXBuildFile; productRef = 513960D72DD8D7F5003E13D9 /* Shared */; }; 15 | 513960DA2DD8D7F5003E13D9 /* UI in Frameworks */ = {isa = PBXBuildFile; productRef = 513960D92DD8D7F5003E13D9 /* UI */; }; 16 | 51FD06162DD92455004BAAAF /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 51FD06152DD92455004BAAAF /* Sparkle */; }; 17 | 70A3845E295644A2000941F5 /* AVKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 70A3845D295644A1000941F5 /* AVKit.framework */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXFileReference section */ 21 | 513960A42DD8788C003E13D9 /* NativeYoutubeKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = NativeYoutubeKit; sourceTree = ""; }; 22 | 70A3845D295644A1000941F5 /* AVKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVKit.framework; path = System/Library/Frameworks/AVKit.framework; sourceTree = SDKROOT; }; 23 | C27DD34A272C853E00B4DC16 /* NativeYoutube.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NativeYoutube.app; sourceTree = BUILT_PRODUCTS_DIR; }; 24 | /* End PBXFileReference section */ 25 | 26 | /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ 27 | 512D78C22DD871B8004C3009 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { 28 | isa = PBXFileSystemSynchronizedBuildFileExceptionSet; 29 | membershipExceptions = ( 30 | Info.plist, 31 | ); 32 | target = C27DD349272C853E00B4DC16 /* NativeYoutube */; 33 | }; 34 | /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ 35 | 36 | /* Begin PBXFileSystemSynchronizedRootGroup section */ 37 | 512D78A12DD871B7004C3009 /* NativeYoutube */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (512D78C22DD871B8004C3009 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = NativeYoutube; sourceTree = ""; }; 38 | /* End PBXFileSystemSynchronizedRootGroup section */ 39 | 40 | /* Begin PBXFrameworksBuildPhase section */ 41 | C27DD347272C853E00B4DC16 /* Frameworks */ = { 42 | isa = PBXFrameworksBuildPhase; 43 | buildActionMask = 2147483647; 44 | files = ( 45 | 512B281A2DD8F8D000D72AB7 /* Clients in Frameworks */, 46 | 51FD06162DD92455004BAAAF /* Sparkle in Frameworks */, 47 | 70A3845E295644A2000941F5 /* AVKit.framework in Frameworks */, 48 | 513960D42DD8D7F5003E13D9 /* Assets in Frameworks */, 49 | 513960D22DD8D7F5003E13D9 /* APIClient in Frameworks */, 50 | 513960DA2DD8D7F5003E13D9 /* UI in Frameworks */, 51 | 513960D82DD8D7F5003E13D9 /* Shared in Frameworks */, 52 | 513960D62DD8D7F5003E13D9 /* Models in Frameworks */, 53 | ); 54 | runOnlyForDeploymentPostprocessing = 0; 55 | }; 56 | /* End PBXFrameworksBuildPhase section */ 57 | 58 | /* Begin PBXGroup section */ 59 | 70A3845C295644A1000941F5 /* Frameworks */ = { 60 | isa = PBXGroup; 61 | children = ( 62 | 70A3845D295644A1000941F5 /* AVKit.framework */, 63 | ); 64 | name = Frameworks; 65 | sourceTree = ""; 66 | }; 67 | C27DD341272C853E00B4DC16 = { 68 | isa = PBXGroup; 69 | children = ( 70 | 513960A42DD8788C003E13D9 /* NativeYoutubeKit */, 71 | 512D78A12DD871B7004C3009 /* NativeYoutube */, 72 | C27DD34B272C853E00B4DC16 /* Products */, 73 | 70A3845C295644A1000941F5 /* Frameworks */, 74 | ); 75 | sourceTree = ""; 76 | }; 77 | C27DD34B272C853E00B4DC16 /* Products */ = { 78 | isa = PBXGroup; 79 | children = ( 80 | C27DD34A272C853E00B4DC16 /* NativeYoutube.app */, 81 | ); 82 | name = Products; 83 | sourceTree = ""; 84 | }; 85 | /* End PBXGroup section */ 86 | 87 | /* Begin PBXNativeTarget section */ 88 | C27DD349272C853E00B4DC16 /* NativeYoutube */ = { 89 | isa = PBXNativeTarget; 90 | buildConfigurationList = C27DD359272C853F00B4DC16 /* Build configuration list for PBXNativeTarget "NativeYoutube" */; 91 | buildPhases = ( 92 | C27DD346272C853E00B4DC16 /* Sources */, 93 | C27DD347272C853E00B4DC16 /* Frameworks */, 94 | C27DD348272C853E00B4DC16 /* Resources */, 95 | ); 96 | buildRules = ( 97 | ); 98 | dependencies = ( 99 | ); 100 | fileSystemSynchronizedGroups = ( 101 | 512D78A12DD871B7004C3009 /* NativeYoutube */, 102 | ); 103 | name = NativeYoutube; 104 | packageProductDependencies = ( 105 | 513960D12DD8D7F5003E13D9 /* APIClient */, 106 | 513960D32DD8D7F5003E13D9 /* Assets */, 107 | 513960D52DD8D7F5003E13D9 /* Models */, 108 | 513960D72DD8D7F5003E13D9 /* Shared */, 109 | 513960D92DD8D7F5003E13D9 /* UI */, 110 | 5170B1662DD8F11100734BCF /* Clients */, 111 | 51FD06152DD92455004BAAAF /* Sparkle */, 112 | ); 113 | productName = NativeYoutube; 114 | productReference = C27DD34A272C853E00B4DC16 /* NativeYoutube.app */; 115 | productType = "com.apple.product-type.application"; 116 | }; 117 | /* End PBXNativeTarget section */ 118 | 119 | /* Begin PBXProject section */ 120 | C27DD342272C853E00B4DC16 /* Project object */ = { 121 | isa = PBXProject; 122 | attributes = { 123 | BuildIndependentTargetsInParallel = 1; 124 | LastSwiftUpdateCheck = 1630; 125 | LastUpgradeCheck = 1630; 126 | TargetAttributes = { 127 | C27DD349272C853E00B4DC16 = { 128 | CreatedOnToolsVersion = 13.1; 129 | }; 130 | }; 131 | }; 132 | buildConfigurationList = C27DD345272C853E00B4DC16 /* Build configuration list for PBXProject "NativeYoutube" */; 133 | compatibilityVersion = "Xcode 13.0"; 134 | developmentRegion = en; 135 | hasScannedForEncodings = 0; 136 | knownRegions = ( 137 | en, 138 | Base, 139 | ); 140 | mainGroup = C27DD341272C853E00B4DC16; 141 | packageReferences = ( 142 | 51FD06142DD92455004BAAAF /* XCRemoteSwiftPackageReference "Sparkle" */, 143 | ); 144 | productRefGroup = C27DD34B272C853E00B4DC16 /* Products */; 145 | projectDirPath = ""; 146 | projectRoot = ""; 147 | targets = ( 148 | C27DD349272C853E00B4DC16 /* NativeYoutube */, 149 | ); 150 | }; 151 | /* End PBXProject section */ 152 | 153 | /* Begin PBXResourcesBuildPhase section */ 154 | C27DD348272C853E00B4DC16 /* Resources */ = { 155 | isa = PBXResourcesBuildPhase; 156 | buildActionMask = 2147483647; 157 | files = ( 158 | ); 159 | runOnlyForDeploymentPostprocessing = 0; 160 | }; 161 | /* End PBXResourcesBuildPhase section */ 162 | 163 | /* Begin PBXSourcesBuildPhase section */ 164 | C27DD346272C853E00B4DC16 /* Sources */ = { 165 | isa = PBXSourcesBuildPhase; 166 | buildActionMask = 2147483647; 167 | files = ( 168 | ); 169 | runOnlyForDeploymentPostprocessing = 0; 170 | }; 171 | /* End PBXSourcesBuildPhase section */ 172 | 173 | /* Begin XCBuildConfiguration section */ 174 | C27DD357272C853F00B4DC16 /* Debug */ = { 175 | isa = XCBuildConfiguration; 176 | buildSettings = { 177 | ALWAYS_SEARCH_USER_PATHS = NO; 178 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 179 | CLANG_ANALYZER_NONNULL = YES; 180 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 181 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 182 | CLANG_CXX_LIBRARY = "libc++"; 183 | CLANG_ENABLE_MODULES = YES; 184 | CLANG_ENABLE_OBJC_ARC = YES; 185 | CLANG_ENABLE_OBJC_WEAK = YES; 186 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 187 | CLANG_WARN_BOOL_CONVERSION = YES; 188 | CLANG_WARN_COMMA = YES; 189 | CLANG_WARN_CONSTANT_CONVERSION = YES; 190 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 191 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 192 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 193 | CLANG_WARN_EMPTY_BODY = YES; 194 | CLANG_WARN_ENUM_CONVERSION = YES; 195 | CLANG_WARN_INFINITE_RECURSION = YES; 196 | CLANG_WARN_INT_CONVERSION = YES; 197 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 198 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 199 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 200 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 201 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 202 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 203 | CLANG_WARN_STRICT_PROTOTYPES = YES; 204 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 205 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 206 | CLANG_WARN_UNREACHABLE_CODE = YES; 207 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 208 | COPY_PHASE_STRIP = NO; 209 | DEAD_CODE_STRIPPING = YES; 210 | DEBUG_INFORMATION_FORMAT = dwarf; 211 | ENABLE_STRICT_OBJC_MSGSEND = YES; 212 | ENABLE_TESTABILITY = YES; 213 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 214 | GCC_C_LANGUAGE_STANDARD = gnu11; 215 | GCC_DYNAMIC_NO_PIC = NO; 216 | GCC_NO_COMMON_BLOCKS = YES; 217 | GCC_OPTIMIZATION_LEVEL = 0; 218 | GCC_PREPROCESSOR_DEFINITIONS = ( 219 | "DEBUG=1", 220 | "$(inherited)", 221 | ); 222 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 223 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 224 | GCC_WARN_UNDECLARED_SELECTOR = YES; 225 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 226 | GCC_WARN_UNUSED_FUNCTION = YES; 227 | GCC_WARN_UNUSED_VARIABLE = YES; 228 | MACOSX_DEPLOYMENT_TARGET = 13.0; 229 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 230 | MTL_FAST_MATH = YES; 231 | ONLY_ACTIVE_ARCH = YES; 232 | SDKROOT = macosx; 233 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 234 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 235 | }; 236 | name = Debug; 237 | }; 238 | C27DD358272C853F00B4DC16 /* Release */ = { 239 | isa = XCBuildConfiguration; 240 | buildSettings = { 241 | ALWAYS_SEARCH_USER_PATHS = NO; 242 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 243 | CLANG_ANALYZER_NONNULL = YES; 244 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 245 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 246 | CLANG_CXX_LIBRARY = "libc++"; 247 | CLANG_ENABLE_MODULES = YES; 248 | CLANG_ENABLE_OBJC_ARC = YES; 249 | CLANG_ENABLE_OBJC_WEAK = YES; 250 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 251 | CLANG_WARN_BOOL_CONVERSION = YES; 252 | CLANG_WARN_COMMA = YES; 253 | CLANG_WARN_CONSTANT_CONVERSION = YES; 254 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 255 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 256 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 257 | CLANG_WARN_EMPTY_BODY = YES; 258 | CLANG_WARN_ENUM_CONVERSION = YES; 259 | CLANG_WARN_INFINITE_RECURSION = YES; 260 | CLANG_WARN_INT_CONVERSION = YES; 261 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 262 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 263 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 264 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 265 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 266 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 267 | CLANG_WARN_STRICT_PROTOTYPES = YES; 268 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 269 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 270 | CLANG_WARN_UNREACHABLE_CODE = YES; 271 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 272 | COPY_PHASE_STRIP = NO; 273 | DEAD_CODE_STRIPPING = YES; 274 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 275 | ENABLE_NS_ASSERTIONS = NO; 276 | ENABLE_STRICT_OBJC_MSGSEND = YES; 277 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 278 | GCC_C_LANGUAGE_STANDARD = gnu11; 279 | GCC_NO_COMMON_BLOCKS = YES; 280 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 281 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 282 | GCC_WARN_UNDECLARED_SELECTOR = YES; 283 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 284 | GCC_WARN_UNUSED_FUNCTION = YES; 285 | GCC_WARN_UNUSED_VARIABLE = YES; 286 | MACOSX_DEPLOYMENT_TARGET = 13.0; 287 | MTL_ENABLE_DEBUG_INFO = NO; 288 | MTL_FAST_MATH = YES; 289 | SDKROOT = macosx; 290 | SWIFT_COMPILATION_MODE = wholemodule; 291 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 292 | }; 293 | name = Release; 294 | }; 295 | C27DD35A272C853F00B4DC16 /* Debug */ = { 296 | isa = XCBuildConfiguration; 297 | buildSettings = { 298 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 299 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 300 | CODE_SIGN_ENTITLEMENTS = NativeYoutube/NativeYoutube.entitlements; 301 | CODE_SIGN_IDENTITY = "-"; 302 | CODE_SIGN_STYLE = Automatic; 303 | COMBINE_HIDPI_IMAGES = YES; 304 | CURRENT_PROJECT_VERSION = 10; 305 | DEAD_CODE_STRIPPING = YES; 306 | DEVELOPMENT_ASSET_PATHS = "\"NativeYoutube/Preview Content\""; 307 | DEVELOPMENT_TEAM = 9RPB76Y973; 308 | ENABLE_HARDENED_RUNTIME = YES; 309 | ENABLE_PREVIEWS = YES; 310 | GENERATE_INFOPLIST_FILE = YES; 311 | INFOPLIST_FILE = NativeYoutube/Info.plist; 312 | INFOPLIST_KEY_CFBundleDisplayName = NativeYoutube; 313 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment"; 314 | INFOPLIST_KEY_LSUIElement = YES; 315 | INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Aayush Pokharel. All rights reserved."; 316 | LD_RUNPATH_SEARCH_PATHS = ( 317 | "$(inherited)", 318 | "@executable_path/../Frameworks", 319 | ); 320 | MACOSX_DEPLOYMENT_TARGET = 14.0; 321 | MARKETING_VERSION = 3.0; 322 | PRODUCT_BUNDLE_IDENTIFIER = com.pokharel.aayush.nativeyoutube; 323 | PRODUCT_NAME = "$(TARGET_NAME)"; 324 | SWIFT_EMIT_LOC_STRINGS = YES; 325 | SWIFT_VERSION = 5.0; 326 | }; 327 | name = Debug; 328 | }; 329 | C27DD35B272C853F00B4DC16 /* Release */ = { 330 | isa = XCBuildConfiguration; 331 | buildSettings = { 332 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 333 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 334 | CODE_SIGN_ENTITLEMENTS = NativeYoutube/NativeYoutube.entitlements; 335 | CODE_SIGN_IDENTITY = "Apple Development"; 336 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; 337 | CODE_SIGN_STYLE = Automatic; 338 | COMBINE_HIDPI_IMAGES = YES; 339 | CURRENT_PROJECT_VERSION = 10; 340 | DEAD_CODE_STRIPPING = YES; 341 | DEVELOPMENT_ASSET_PATHS = "\"NativeYoutube/Preview Content\""; 342 | DEVELOPMENT_TEAM = 9RPB76Y973; 343 | ENABLE_HARDENED_RUNTIME = YES; 344 | ENABLE_PREVIEWS = YES; 345 | GENERATE_INFOPLIST_FILE = YES; 346 | INFOPLIST_FILE = NativeYoutube/Info.plist; 347 | INFOPLIST_KEY_CFBundleDisplayName = NativeYoutube; 348 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment"; 349 | INFOPLIST_KEY_LSUIElement = YES; 350 | INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Aayush Pokharel. All rights reserved."; 351 | LD_RUNPATH_SEARCH_PATHS = ( 352 | "$(inherited)", 353 | "@executable_path/../Frameworks", 354 | ); 355 | MACOSX_DEPLOYMENT_TARGET = 14.0; 356 | MARKETING_VERSION = 3.0; 357 | PRODUCT_BUNDLE_IDENTIFIER = com.pokharel.aayush.nativeyoutube; 358 | PRODUCT_NAME = "$(TARGET_NAME)"; 359 | SWIFT_EMIT_LOC_STRINGS = YES; 360 | SWIFT_VERSION = 5.0; 361 | }; 362 | name = Release; 363 | }; 364 | /* End XCBuildConfiguration section */ 365 | 366 | /* Begin XCConfigurationList section */ 367 | C27DD345272C853E00B4DC16 /* Build configuration list for PBXProject "NativeYoutube" */ = { 368 | isa = XCConfigurationList; 369 | buildConfigurations = ( 370 | C27DD357272C853F00B4DC16 /* Debug */, 371 | C27DD358272C853F00B4DC16 /* Release */, 372 | ); 373 | defaultConfigurationIsVisible = 0; 374 | defaultConfigurationName = Release; 375 | }; 376 | C27DD359272C853F00B4DC16 /* Build configuration list for PBXNativeTarget "NativeYoutube" */ = { 377 | isa = XCConfigurationList; 378 | buildConfigurations = ( 379 | C27DD35A272C853F00B4DC16 /* Debug */, 380 | C27DD35B272C853F00B4DC16 /* Release */, 381 | ); 382 | defaultConfigurationIsVisible = 0; 383 | defaultConfigurationName = Release; 384 | }; 385 | /* End XCConfigurationList section */ 386 | 387 | /* Begin XCRemoteSwiftPackageReference section */ 388 | 51FD06142DD92455004BAAAF /* XCRemoteSwiftPackageReference "Sparkle" */ = { 389 | isa = XCRemoteSwiftPackageReference; 390 | repositoryURL = "https://github.com/sparkle-project/Sparkle"; 391 | requirement = { 392 | kind = upToNextMajorVersion; 393 | minimumVersion = 2.7.0; 394 | }; 395 | }; 396 | /* End XCRemoteSwiftPackageReference section */ 397 | 398 | /* Begin XCSwiftPackageProductDependency section */ 399 | 513960D12DD8D7F5003E13D9 /* APIClient */ = { 400 | isa = XCSwiftPackageProductDependency; 401 | productName = APIClient; 402 | }; 403 | 513960D32DD8D7F5003E13D9 /* Assets */ = { 404 | isa = XCSwiftPackageProductDependency; 405 | productName = Assets; 406 | }; 407 | 513960D52DD8D7F5003E13D9 /* Models */ = { 408 | isa = XCSwiftPackageProductDependency; 409 | productName = Models; 410 | }; 411 | 513960D72DD8D7F5003E13D9 /* Shared */ = { 412 | isa = XCSwiftPackageProductDependency; 413 | productName = Shared; 414 | }; 415 | 513960D92DD8D7F5003E13D9 /* UI */ = { 416 | isa = XCSwiftPackageProductDependency; 417 | productName = UI; 418 | }; 419 | 5170B1662DD8F11100734BCF /* Clients */ = { 420 | isa = XCSwiftPackageProductDependency; 421 | productName = Clients; 422 | }; 423 | 51FD06152DD92455004BAAAF /* Sparkle */ = { 424 | isa = XCSwiftPackageProductDependency; 425 | package = 51FD06142DD92455004BAAAF /* XCRemoteSwiftPackageReference "Sparkle" */; 426 | productName = Sparkle; 427 | }; 428 | /* End XCSwiftPackageProductDependency section */ 429 | }; 430 | rootObject = C27DD342272C853E00B4DC16 /* Project object */; 431 | } 432 | -------------------------------------------------------------------------------- /NativeYoutube.xcodeproj/project.pbxproj.backup: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 70; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 513960D22DD8D7F5003E13D9 /* APIClient in Frameworks */ = {isa = PBXBuildFile; productRef = 513960D12DD8D7F5003E13D9 /* APIClient */; }; 11 | 513960D42DD8D7F5003E13D9 /* Assets in Frameworks */ = {isa = PBXBuildFile; productRef = 513960D32DD8D7F5003E13D9 /* Assets */; }; 12 | 513960D62DD8D7F5003E13D9 /* Models in Frameworks */ = {isa = PBXBuildFile; productRef = 513960D52DD8D7F5003E13D9 /* Models */; }; 13 | 513960D82DD8D7F5003E13D9 /* Shared in Frameworks */ = {isa = PBXBuildFile; productRef = 513960D72DD8D7F5003E13D9 /* Shared */; }; 14 | 513960DA2DD8D7F5003E13D9 /* UI in Frameworks */ = {isa = PBXBuildFile; productRef = 513960D92DD8D7F5003E13D9 /* UI */; }; 15 | 5170B1612DD8F11000734BCF /* APIClient in Frameworks */ = {isa = PBXBuildFile; productRef = 5170B1602DD8F11000734BCF /* APIClient */; }; 16 | 5170B1632DD8F11000734BCF /* Models in Frameworks */ = {isa = PBXBuildFile; productRef = 5170B1622DD8F11000734BCF /* Models */; }; 17 | 5170B1652DD8F11000734BCF /* Shared in Frameworks */ = {isa = PBXBuildFile; productRef = 5170B1642DD8F11000734BCF /* Shared */; }; 18 | 70A3845E295644A2000941F5 /* AVKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 70A3845D295644A1000941F5 /* AVKit.framework */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXContainerItemProxy section */ 22 | 5170B1472DD8EDCE00734BCF /* PBXContainerItemProxy */ = { 23 | isa = PBXContainerItemProxy; 24 | containerPortal = C27DD342272C853E00B4DC16 /* Project object */; 25 | proxyType = 1; 26 | remoteGlobalIDString = C27DD349272C853E00B4DC16; 27 | remoteInfo = NativeYoutube; 28 | }; 29 | /* End PBXContainerItemProxy section */ 30 | 31 | /* Begin PBXFileReference section */ 32 | 513960A42DD8788C003E13D9 /* NativeYoutubeKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = NativeYoutubeKit; sourceTree = ""; }; 33 | 5170B1432DD8EDCE00734BCF /* NativeYoutubeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NativeYoutubeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 34 | 70A3845D295644A1000941F5 /* AVKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVKit.framework; path = System/Library/Frameworks/AVKit.framework; sourceTree = SDKROOT; }; 35 | C27DD34A272C853E00B4DC16 /* NativeYoutube.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NativeYoutube.app; sourceTree = BUILT_PRODUCTS_DIR; }; 36 | /* End PBXFileReference section */ 37 | 38 | /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ 39 | 512D78C22DD871B8004C3009 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { 40 | isa = PBXFileSystemSynchronizedBuildFileExceptionSet; 41 | membershipExceptions = ( 42 | Info.plist, 43 | ); 44 | target = C27DD349272C853E00B4DC16 /* NativeYoutube */; 45 | }; 46 | /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ 47 | 48 | /* Begin PBXFileSystemSynchronizedRootGroup section */ 49 | 512D78A12DD871B7004C3009 /* NativeYoutube */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (512D78C22DD871B8004C3009 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = NativeYoutube; sourceTree = ""; }; 50 | 5170B1442DD8EDCE00734BCF /* NativeYoutubeTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = NativeYoutubeTests; sourceTree = ""; }; 51 | /* End PBXFileSystemSynchronizedRootGroup section */ 52 | 53 | /* Begin PBXFrameworksBuildPhase section */ 54 | 5170B1402DD8EDCE00734BCF /* Frameworks */ = { 55 | isa = PBXFrameworksBuildPhase; 56 | buildActionMask = 2147483647; 57 | files = ( 58 | 5170B1632DD8F11000734BCF /* Models in Frameworks */, 59 | 5170B1612DD8F11000734BCF /* APIClient in Frameworks */, 60 | 5170B1652DD8F11000734BCF /* Shared in Frameworks */, 61 | ); 62 | runOnlyForDeploymentPostprocessing = 0; 63 | }; 64 | C27DD347272C853E00B4DC16 /* Frameworks */ = { 65 | isa = PBXFrameworksBuildPhase; 66 | buildActionMask = 2147483647; 67 | files = ( 68 | 70A3845E295644A2000941F5 /* AVKit.framework in Frameworks */, 69 | 513960D42DD8D7F5003E13D9 /* Assets in Frameworks */, 70 | 513960D22DD8D7F5003E13D9 /* APIClient in Frameworks */, 71 | 513960DA2DD8D7F5003E13D9 /* UI in Frameworks */, 72 | 513960D82DD8D7F5003E13D9 /* Shared in Frameworks */, 73 | 513960D62DD8D7F5003E13D9 /* Models in Frameworks */, 74 | ); 75 | runOnlyForDeploymentPostprocessing = 0; 76 | }; 77 | /* End PBXFrameworksBuildPhase section */ 78 | 79 | /* Begin PBXGroup section */ 80 | 70A3845C295644A1000941F5 /* Frameworks */ = { 81 | isa = PBXGroup; 82 | children = ( 83 | 70A3845D295644A1000941F5 /* AVKit.framework */, 84 | ); 85 | name = Frameworks; 86 | sourceTree = ""; 87 | }; 88 | C27DD341272C853E00B4DC16 = { 89 | isa = PBXGroup; 90 | children = ( 91 | 513960A42DD8788C003E13D9 /* NativeYoutubeKit */, 92 | 512D78A12DD871B7004C3009 /* NativeYoutube */, 93 | 5170B1442DD8EDCE00734BCF /* NativeYoutubeTests */, 94 | C27DD34B272C853E00B4DC16 /* Products */, 95 | 70A3845C295644A1000941F5 /* Frameworks */, 96 | ); 97 | sourceTree = ""; 98 | }; 99 | C27DD34B272C853E00B4DC16 /* Products */ = { 100 | isa = PBXGroup; 101 | children = ( 102 | C27DD34A272C853E00B4DC16 /* NativeYoutube.app */, 103 | 5170B1432DD8EDCE00734BCF /* NativeYoutubeTests.xctest */, 104 | ); 105 | name = Products; 106 | sourceTree = ""; 107 | }; 108 | /* End PBXGroup section */ 109 | 110 | /* Begin PBXNativeTarget section */ 111 | 5170B1422DD8EDCE00734BCF /* NativeYoutubeTests */ = { 112 | isa = PBXNativeTarget; 113 | buildConfigurationList = 5170B1492DD8EDCE00734BCF /* Build configuration list for PBXNativeTarget "NativeYoutubeTests" */; 114 | buildPhases = ( 115 | 5170B13F2DD8EDCE00734BCF /* Sources */, 116 | 5170B1402DD8EDCE00734BCF /* Frameworks */, 117 | 5170B1412DD8EDCE00734BCF /* Resources */, 118 | ); 119 | buildRules = ( 120 | ); 121 | dependencies = ( 122 | 5170B1482DD8EDCE00734BCF /* PBXTargetDependency */, 123 | ); 124 | fileSystemSynchronizedGroups = ( 125 | 5170B1442DD8EDCE00734BCF /* NativeYoutubeTests */, 126 | ); 127 | name = NativeYoutubeTests; 128 | packageProductDependencies = ( 129 | 5170B1602DD8F11000734BCF /* APIClient */, 130 | 5170B1622DD8F11000734BCF /* Models */, 131 | 5170B1642DD8F11000734BCF /* Shared */, 132 | ); 133 | productName = NativeYoutubeTests; 134 | productReference = 5170B1432DD8EDCE00734BCF /* NativeYoutubeTests.xctest */; 135 | productType = "com.apple.product-type.bundle.unit-test"; 136 | }; 137 | C27DD349272C853E00B4DC16 /* NativeYoutube */ = { 138 | isa = PBXNativeTarget; 139 | buildConfigurationList = C27DD359272C853F00B4DC16 /* Build configuration list for PBXNativeTarget "NativeYoutube" */; 140 | buildPhases = ( 141 | C27DD346272C853E00B4DC16 /* Sources */, 142 | C27DD347272C853E00B4DC16 /* Frameworks */, 143 | C27DD348272C853E00B4DC16 /* Resources */, 144 | ); 145 | buildRules = ( 146 | ); 147 | dependencies = ( 148 | ); 149 | fileSystemSynchronizedGroups = ( 150 | 512D78A12DD871B7004C3009 /* NativeYoutube */, 151 | ); 152 | name = NativeYoutube; 153 | packageProductDependencies = ( 154 | 513960D12DD8D7F5003E13D9 /* APIClient */, 155 | 513960D32DD8D7F5003E13D9 /* Assets */, 156 | 513960D52DD8D7F5003E13D9 /* Models */, 157 | 513960D72DD8D7F5003E13D9 /* Shared */, 158 | 513960D92DD8D7F5003E13D9 /* UI */, 159 | ); 160 | productName = NativeYoutube; 161 | productReference = C27DD34A272C853E00B4DC16 /* NativeYoutube.app */; 162 | productType = "com.apple.product-type.application"; 163 | }; 164 | /* End PBXNativeTarget section */ 165 | 166 | /* Begin PBXProject section */ 167 | C27DD342272C853E00B4DC16 /* Project object */ = { 168 | isa = PBXProject; 169 | attributes = { 170 | BuildIndependentTargetsInParallel = 1; 171 | LastSwiftUpdateCheck = 1630; 172 | LastUpgradeCheck = 1630; 173 | TargetAttributes = { 174 | 5170B1422DD8EDCE00734BCF = { 175 | CreatedOnToolsVersion = 16.3; 176 | TestTargetID = C27DD349272C853E00B4DC16; 177 | }; 178 | C27DD349272C853E00B4DC16 = { 179 | CreatedOnToolsVersion = 13.1; 180 | }; 181 | }; 182 | }; 183 | buildConfigurationList = C27DD345272C853E00B4DC16 /* Build configuration list for PBXProject "NativeYoutube" */; 184 | compatibilityVersion = "Xcode 13.0"; 185 | developmentRegion = en; 186 | hasScannedForEncodings = 0; 187 | knownRegions = ( 188 | en, 189 | Base, 190 | ); 191 | mainGroup = C27DD341272C853E00B4DC16; 192 | packageReferences = ( 193 | ); 194 | productRefGroup = C27DD34B272C853E00B4DC16 /* Products */; 195 | projectDirPath = ""; 196 | projectRoot = ""; 197 | targets = ( 198 | C27DD349272C853E00B4DC16 /* NativeYoutube */, 199 | 5170B1422DD8EDCE00734BCF /* NativeYoutubeTests */, 200 | ); 201 | }; 202 | /* End PBXProject section */ 203 | 204 | /* Begin PBXResourcesBuildPhase section */ 205 | 5170B1412DD8EDCE00734BCF /* Resources */ = { 206 | isa = PBXResourcesBuildPhase; 207 | buildActionMask = 2147483647; 208 | files = ( 209 | ); 210 | runOnlyForDeploymentPostprocessing = 0; 211 | }; 212 | C27DD348272C853E00B4DC16 /* Resources */ = { 213 | isa = PBXResourcesBuildPhase; 214 | buildActionMask = 2147483647; 215 | files = ( 216 | ); 217 | runOnlyForDeploymentPostprocessing = 0; 218 | }; 219 | /* End PBXResourcesBuildPhase section */ 220 | 221 | /* Begin PBXSourcesBuildPhase section */ 222 | 5170B13F2DD8EDCE00734BCF /* Sources */ = { 223 | isa = PBXSourcesBuildPhase; 224 | buildActionMask = 2147483647; 225 | files = ( 226 | ); 227 | runOnlyForDeploymentPostprocessing = 0; 228 | }; 229 | C27DD346272C853E00B4DC16 /* Sources */ = { 230 | isa = PBXSourcesBuildPhase; 231 | buildActionMask = 2147483647; 232 | files = ( 233 | ); 234 | runOnlyForDeploymentPostprocessing = 0; 235 | }; 236 | /* End PBXSourcesBuildPhase section */ 237 | 238 | /* Begin PBXTargetDependency section */ 239 | 5170B1482DD8EDCE00734BCF /* PBXTargetDependency */ = { 240 | isa = PBXTargetDependency; 241 | target = C27DD349272C853E00B4DC16 /* NativeYoutube */; 242 | targetProxy = 5170B1472DD8EDCE00734BCF /* PBXContainerItemProxy */; 243 | }; 244 | /* End PBXTargetDependency section */ 245 | 246 | /* Begin XCBuildConfiguration section */ 247 | 5170B14A2DD8EDCE00734BCF /* Debug */ = { 248 | isa = XCBuildConfiguration; 249 | buildSettings = { 250 | BUNDLE_LOADER = "$(TEST_HOST)"; 251 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 252 | CODE_SIGN_STYLE = Automatic; 253 | CURRENT_PROJECT_VERSION = 1; 254 | DEVELOPMENT_TEAM = 9RPB76Y973; 255 | GCC_C_LANGUAGE_STANDARD = gnu17; 256 | GENERATE_INFOPLIST_FILE = YES; 257 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 258 | MACOSX_DEPLOYMENT_TARGET = 14.0; 259 | MARKETING_VERSION = 1.0; 260 | PRODUCT_BUNDLE_IDENTIFIER = com.pokharel.aayush.NativeYoutubeTests; 261 | PRODUCT_NAME = "$(TARGET_NAME)"; 262 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 263 | SWIFT_EMIT_LOC_STRINGS = NO; 264 | SWIFT_VERSION = 5.0; 265 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/NativeYoutube.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/NativeYoutube"; 266 | }; 267 | name = Debug; 268 | }; 269 | 5170B14B2DD8EDCE00734BCF /* Release */ = { 270 | isa = XCBuildConfiguration; 271 | buildSettings = { 272 | BUNDLE_LOADER = "$(TEST_HOST)"; 273 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 274 | CODE_SIGN_STYLE = Automatic; 275 | CURRENT_PROJECT_VERSION = 1; 276 | DEVELOPMENT_TEAM = 9RPB76Y973; 277 | GCC_C_LANGUAGE_STANDARD = gnu17; 278 | GENERATE_INFOPLIST_FILE = YES; 279 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 280 | MACOSX_DEPLOYMENT_TARGET = 14.0; 281 | MARKETING_VERSION = 1.0; 282 | PRODUCT_BUNDLE_IDENTIFIER = com.pokharel.aayush.NativeYoutubeTests; 283 | PRODUCT_NAME = "$(TARGET_NAME)"; 284 | SWIFT_EMIT_LOC_STRINGS = NO; 285 | SWIFT_VERSION = 5.0; 286 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/NativeYoutube.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/NativeYoutube"; 287 | }; 288 | name = Release; 289 | }; 290 | C27DD357272C853F00B4DC16 /* Debug */ = { 291 | isa = XCBuildConfiguration; 292 | buildSettings = { 293 | ALWAYS_SEARCH_USER_PATHS = NO; 294 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 295 | CLANG_ANALYZER_NONNULL = YES; 296 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 297 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 298 | CLANG_CXX_LIBRARY = "libc++"; 299 | CLANG_ENABLE_MODULES = YES; 300 | CLANG_ENABLE_OBJC_ARC = YES; 301 | CLANG_ENABLE_OBJC_WEAK = YES; 302 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 303 | CLANG_WARN_BOOL_CONVERSION = YES; 304 | CLANG_WARN_COMMA = YES; 305 | CLANG_WARN_CONSTANT_CONVERSION = YES; 306 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 307 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 308 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 309 | CLANG_WARN_EMPTY_BODY = YES; 310 | CLANG_WARN_ENUM_CONVERSION = YES; 311 | CLANG_WARN_INFINITE_RECURSION = YES; 312 | CLANG_WARN_INT_CONVERSION = YES; 313 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 314 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 315 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 316 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 317 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 318 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 319 | CLANG_WARN_STRICT_PROTOTYPES = YES; 320 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 321 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 322 | CLANG_WARN_UNREACHABLE_CODE = YES; 323 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 324 | COPY_PHASE_STRIP = NO; 325 | DEAD_CODE_STRIPPING = YES; 326 | DEBUG_INFORMATION_FORMAT = dwarf; 327 | ENABLE_STRICT_OBJC_MSGSEND = YES; 328 | ENABLE_TESTABILITY = YES; 329 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 330 | GCC_C_LANGUAGE_STANDARD = gnu11; 331 | GCC_DYNAMIC_NO_PIC = NO; 332 | GCC_NO_COMMON_BLOCKS = YES; 333 | GCC_OPTIMIZATION_LEVEL = 0; 334 | GCC_PREPROCESSOR_DEFINITIONS = ( 335 | "DEBUG=1", 336 | "$(inherited)", 337 | ); 338 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 339 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 340 | GCC_WARN_UNDECLARED_SELECTOR = YES; 341 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 342 | GCC_WARN_UNUSED_FUNCTION = YES; 343 | GCC_WARN_UNUSED_VARIABLE = YES; 344 | MACOSX_DEPLOYMENT_TARGET = 13.0; 345 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 346 | MTL_FAST_MATH = YES; 347 | ONLY_ACTIVE_ARCH = YES; 348 | SDKROOT = macosx; 349 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 350 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 351 | }; 352 | name = Debug; 353 | }; 354 | C27DD358272C853F00B4DC16 /* Release */ = { 355 | isa = XCBuildConfiguration; 356 | buildSettings = { 357 | ALWAYS_SEARCH_USER_PATHS = NO; 358 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 359 | CLANG_ANALYZER_NONNULL = YES; 360 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 361 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 362 | CLANG_CXX_LIBRARY = "libc++"; 363 | CLANG_ENABLE_MODULES = YES; 364 | CLANG_ENABLE_OBJC_ARC = YES; 365 | CLANG_ENABLE_OBJC_WEAK = YES; 366 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 367 | CLANG_WARN_BOOL_CONVERSION = YES; 368 | CLANG_WARN_COMMA = YES; 369 | CLANG_WARN_CONSTANT_CONVERSION = YES; 370 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 371 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 372 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 373 | CLANG_WARN_EMPTY_BODY = YES; 374 | CLANG_WARN_ENUM_CONVERSION = YES; 375 | CLANG_WARN_INFINITE_RECURSION = YES; 376 | CLANG_WARN_INT_CONVERSION = YES; 377 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 378 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 379 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 380 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 381 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 382 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 383 | CLANG_WARN_STRICT_PROTOTYPES = YES; 384 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 385 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 386 | CLANG_WARN_UNREACHABLE_CODE = YES; 387 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 388 | COPY_PHASE_STRIP = NO; 389 | DEAD_CODE_STRIPPING = YES; 390 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 391 | ENABLE_NS_ASSERTIONS = NO; 392 | ENABLE_STRICT_OBJC_MSGSEND = YES; 393 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 394 | GCC_C_LANGUAGE_STANDARD = gnu11; 395 | GCC_NO_COMMON_BLOCKS = YES; 396 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 397 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 398 | GCC_WARN_UNDECLARED_SELECTOR = YES; 399 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 400 | GCC_WARN_UNUSED_FUNCTION = YES; 401 | GCC_WARN_UNUSED_VARIABLE = YES; 402 | MACOSX_DEPLOYMENT_TARGET = 13.0; 403 | MTL_ENABLE_DEBUG_INFO = NO; 404 | MTL_FAST_MATH = YES; 405 | SDKROOT = macosx; 406 | SWIFT_COMPILATION_MODE = wholemodule; 407 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 408 | }; 409 | name = Release; 410 | }; 411 | C27DD35A272C853F00B4DC16 /* Debug */ = { 412 | isa = XCBuildConfiguration; 413 | buildSettings = { 414 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 415 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 416 | CODE_SIGN_ENTITLEMENTS = NativeYoutube/NativeYoutube.entitlements; 417 | CODE_SIGN_IDENTITY = "-"; 418 | CODE_SIGN_STYLE = Automatic; 419 | COMBINE_HIDPI_IMAGES = YES; 420 | CURRENT_PROJECT_VERSION = 10; 421 | DEAD_CODE_STRIPPING = YES; 422 | DEVELOPMENT_ASSET_PATHS = "\"NativeYoutube/Preview Content\""; 423 | DEVELOPMENT_TEAM = ""; 424 | ENABLE_HARDENED_RUNTIME = YES; 425 | ENABLE_PREVIEWS = YES; 426 | GENERATE_INFOPLIST_FILE = YES; 427 | INFOPLIST_FILE = NativeYoutube/Info.plist; 428 | INFOPLIST_KEY_CFBundleDisplayName = NativeYoutube; 429 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment"; 430 | INFOPLIST_KEY_LSUIElement = YES; 431 | INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Aayush Pokharel. All rights reserved."; 432 | LD_RUNPATH_SEARCH_PATHS = ( 433 | "$(inherited)", 434 | "@executable_path/../Frameworks", 435 | ); 436 | MACOSX_DEPLOYMENT_TARGET = 14.0; 437 | MARKETING_VERSION = 3.0; 438 | PRODUCT_BUNDLE_IDENTIFIER = com.pokharel.aayush.nativeyoutube; 439 | PRODUCT_NAME = "$(TARGET_NAME)"; 440 | SWIFT_EMIT_LOC_STRINGS = YES; 441 | SWIFT_VERSION = 5.0; 442 | }; 443 | name = Debug; 444 | }; 445 | C27DD35B272C853F00B4DC16 /* Release */ = { 446 | isa = XCBuildConfiguration; 447 | buildSettings = { 448 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 449 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 450 | CODE_SIGN_ENTITLEMENTS = NativeYoutube/NativeYoutube.entitlements; 451 | CODE_SIGN_IDENTITY = "Apple Development"; 452 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; 453 | CODE_SIGN_STYLE = Automatic; 454 | COMBINE_HIDPI_IMAGES = YES; 455 | CURRENT_PROJECT_VERSION = 10; 456 | DEAD_CODE_STRIPPING = YES; 457 | DEVELOPMENT_ASSET_PATHS = "\"NativeYoutube/Preview Content\""; 458 | DEVELOPMENT_TEAM = ""; 459 | ENABLE_HARDENED_RUNTIME = YES; 460 | ENABLE_PREVIEWS = YES; 461 | GENERATE_INFOPLIST_FILE = YES; 462 | INFOPLIST_FILE = NativeYoutube/Info.plist; 463 | INFOPLIST_KEY_CFBundleDisplayName = NativeYoutube; 464 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment"; 465 | INFOPLIST_KEY_LSUIElement = YES; 466 | INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Aayush Pokharel. All rights reserved."; 467 | LD_RUNPATH_SEARCH_PATHS = ( 468 | "$(inherited)", 469 | "@executable_path/../Frameworks", 470 | ); 471 | MACOSX_DEPLOYMENT_TARGET = 14.0; 472 | MARKETING_VERSION = 3.0; 473 | PRODUCT_BUNDLE_IDENTIFIER = com.pokharel.aayush.nativeyoutube; 474 | PRODUCT_NAME = "$(TARGET_NAME)"; 475 | SWIFT_EMIT_LOC_STRINGS = YES; 476 | SWIFT_VERSION = 5.0; 477 | }; 478 | name = Release; 479 | }; 480 | /* End XCBuildConfiguration section */ 481 | 482 | /* Begin XCConfigurationList section */ 483 | 5170B1492DD8EDCE00734BCF /* Build configuration list for PBXNativeTarget "NativeYoutubeTests" */ = { 484 | isa = XCConfigurationList; 485 | buildConfigurations = ( 486 | 5170B14A2DD8EDCE00734BCF /* Debug */, 487 | 5170B14B2DD8EDCE00734BCF /* Release */, 488 | ); 489 | defaultConfigurationIsVisible = 0; 490 | defaultConfigurationName = Release; 491 | }; 492 | C27DD345272C853E00B4DC16 /* Build configuration list for PBXProject "NativeYoutube" */ = { 493 | isa = XCConfigurationList; 494 | buildConfigurations = ( 495 | C27DD357272C853F00B4DC16 /* Debug */, 496 | C27DD358272C853F00B4DC16 /* Release */, 497 | ); 498 | defaultConfigurationIsVisible = 0; 499 | defaultConfigurationName = Release; 500 | }; 501 | C27DD359272C853F00B4DC16 /* Build configuration list for PBXNativeTarget "NativeYoutube" */ = { 502 | isa = XCConfigurationList; 503 | buildConfigurations = ( 504 | C27DD35A272C853F00B4DC16 /* Debug */, 505 | C27DD35B272C853F00B4DC16 /* Release */, 506 | ); 507 | defaultConfigurationIsVisible = 0; 508 | defaultConfigurationName = Release; 509 | }; 510 | /* End XCConfigurationList section */ 511 | 512 | /* Begin XCSwiftPackageProductDependency section */ 513 | 513960D12DD8D7F5003E13D9 /* APIClient */ = { 514 | isa = XCSwiftPackageProductDependency; 515 | productName = APIClient; 516 | }; 517 | 513960D32DD8D7F5003E13D9 /* Assets */ = { 518 | isa = XCSwiftPackageProductDependency; 519 | productName = Assets; 520 | }; 521 | 513960D52DD8D7F5003E13D9 /* Models */ = { 522 | isa = XCSwiftPackageProductDependency; 523 | productName = Models; 524 | }; 525 | 513960D72DD8D7F5003E13D9 /* Shared */ = { 526 | isa = XCSwiftPackageProductDependency; 527 | productName = Shared; 528 | }; 529 | 513960D92DD8D7F5003E13D9 /* UI */ = { 530 | isa = XCSwiftPackageProductDependency; 531 | productName = UI; 532 | }; 533 | 5170B1602DD8F11000734BCF /* APIClient */ = { 534 | isa = XCSwiftPackageProductDependency; 535 | productName = APIClient; 536 | }; 537 | 5170B1622DD8F11000734BCF /* Models */ = { 538 | isa = XCSwiftPackageProductDependency; 539 | productName = Models; 540 | }; 541 | 5170B1642DD8F11000734BCF /* Shared */ = { 542 | isa = XCSwiftPackageProductDependency; 543 | productName = Shared; 544 | }; 545 | /* End XCSwiftPackageProductDependency section */ 546 | }; 547 | rootObject = C27DD342272C853E00B4DC16 /* Project object */; 548 | } 549 | -------------------------------------------------------------------------------- /NativeYoutube.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /NativeYoutube.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /NativeYoutube.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "e61f3da8a28780725c42ed6468849c3f7ad8955489ee3ffab16117f9d90fbded", 3 | "pins" : [ 4 | { 5 | "identity" : "combine-schedulers", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/pointfreeco/combine-schedulers", 8 | "state" : { 9 | "revision" : "5928286acce13def418ec36d05a001a9641086f2", 10 | "version" : "1.0.3" 11 | } 12 | }, 13 | { 14 | "identity" : "sparkle", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/sparkle-project/Sparkle", 17 | "state" : { 18 | "revision" : "0ca3004e98712ea2b39dd881d28448630cce1c99", 19 | "version" : "2.7.0" 20 | } 21 | }, 22 | { 23 | "identity" : "swift-clocks", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/pointfreeco/swift-clocks", 26 | "state" : { 27 | "revision" : "cc46202b53476d64e824e0b6612da09d84ffde8e", 28 | "version" : "1.0.6" 29 | } 30 | }, 31 | { 32 | "identity" : "swift-collections", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/apple/swift-collections", 35 | "state" : { 36 | "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", 37 | "version" : "1.1.4" 38 | } 39 | }, 40 | { 41 | "identity" : "swift-concurrency-extras", 42 | "kind" : "remoteSourceControl", 43 | "location" : "https://github.com/pointfreeco/swift-concurrency-extras", 44 | "state" : { 45 | "revision" : "82a4ae7170d98d8538ec77238b7eb8e7199ef2e8", 46 | "version" : "1.3.1" 47 | } 48 | }, 49 | { 50 | "identity" : "swift-custom-dump", 51 | "kind" : "remoteSourceControl", 52 | "location" : "https://github.com/pointfreeco/swift-custom-dump", 53 | "state" : { 54 | "revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1", 55 | "version" : "1.3.3" 56 | } 57 | }, 58 | { 59 | "identity" : "swift-dependencies", 60 | "kind" : "remoteSourceControl", 61 | "location" : "https://github.com/pointfreeco/swift-dependencies", 62 | "state" : { 63 | "revision" : "4c90d6b2b9bf0911af87b103bb40f41771891596", 64 | "version" : "1.9.2" 65 | } 66 | }, 67 | { 68 | "identity" : "swift-identified-collections", 69 | "kind" : "remoteSourceControl", 70 | "location" : "https://github.com/pointfreeco/swift-identified-collections", 71 | "state" : { 72 | "revision" : "322d9ffeeba85c9f7c4984b39422ec7cc3c56597", 73 | "version" : "1.1.1" 74 | } 75 | }, 76 | { 77 | "identity" : "swift-perception", 78 | "kind" : "remoteSourceControl", 79 | "location" : "https://github.com/pointfreeco/swift-perception", 80 | "state" : { 81 | "revision" : "d924c62a70fca5f43872f286dbd7cef0957f1c01", 82 | "version" : "1.6.0" 83 | } 84 | }, 85 | { 86 | "identity" : "swift-sharing", 87 | "kind" : "remoteSourceControl", 88 | "location" : "https://github.com/pointfreeco/swift-sharing.git", 89 | "state" : { 90 | "revision" : "75e846ee3159dc75b3a29bfc24b6ce5a557ddca9", 91 | "version" : "2.5.2" 92 | } 93 | }, 94 | { 95 | "identity" : "swift-syntax", 96 | "kind" : "remoteSourceControl", 97 | "location" : "https://github.com/swiftlang/swift-syntax", 98 | "state" : { 99 | "revision" : "f99ae8aa18f0cf0d53481901f88a0991dc3bd4a2", 100 | "version" : "601.0.1" 101 | } 102 | }, 103 | { 104 | "identity" : "xctest-dynamic-overlay", 105 | "kind" : "remoteSourceControl", 106 | "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", 107 | "state" : { 108 | "revision" : "39de59b2d47f7ef3ca88a039dff3084688fe27f4", 109 | "version" : "1.5.2" 110 | } 111 | }, 112 | { 113 | "identity" : "youtubekit", 114 | "kind" : "remoteSourceControl", 115 | "location" : "https://github.com/alexeichhorn/YouTubeKit", 116 | "state" : { 117 | "revision" : "e1895c72f0edff9acd6b41977d2ad9c6245e8306", 118 | "version" : "0.2.7" 119 | } 120 | } 121 | ], 122 | "version" : 3 123 | } 124 | -------------------------------------------------------------------------------- /NativeYoutube.xcodeproj/project.xcworkspace/xcuserdata/aayush29.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aayush9029/NativeYoutube/3c42cbbd5f8b06a90165b97ae6064d45940482bc/NativeYoutube.xcodeproj/project.xcworkspace/xcuserdata/aayush29.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /NativeYoutube.xcodeproj/project.xcworkspace/xcuserdata/aayushpokharel.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aayush9029/NativeYoutube/3c42cbbd5f8b06a90165b97ae6064d45940482bc/NativeYoutube.xcodeproj/project.xcworkspace/xcuserdata/aayushpokharel.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /NativeYoutube.xcodeproj/xcshareddata/xcschemes/NativeYoutube.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 36 | 42 | 43 | 44 | 45 | 46 | 56 | 58 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /NativeYoutube.xcodeproj/xcuserdata/aayush29.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | NativeYoutube.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | Playground (Playground) 1.xcscheme 13 | 14 | isShown 15 | 16 | orderHint 17 | 2 18 | 19 | Playground (Playground) 2.xcscheme 20 | 21 | isShown 22 | 23 | orderHint 24 | 3 25 | 26 | Playground (Playground).xcscheme 27 | 28 | isShown 29 | 30 | orderHint 31 | 1 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /NativeYoutube.xcodeproj/xcuserdata/aayushpokharel.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /NativeYoutube.xcodeproj/xcuserdata/aayushpokharel.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | NativeYoutube.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | Playground (Playground) 1.xcscheme 13 | 14 | isShown 15 | 16 | orderHint 17 | 2 18 | 19 | Playground (Playground) 2.xcscheme 20 | 21 | isShown 22 | 23 | orderHint 24 | 3 25 | 26 | Playground (Playground).xcscheme 27 | 28 | isShown 29 | 30 | orderHint 31 | 1 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /NativeYoutube/AppCoordinator.swift: -------------------------------------------------------------------------------- 1 | import Clients 2 | import Dependencies 3 | import Models 4 | import Shared 5 | import SwiftUI 6 | import Sparkle 7 | 8 | @MainActor 9 | final class AppCoordinator: ObservableObject { 10 | @Published var currentPage: Pages = .playlists 11 | @Published var searchQuery: String = "" 12 | @Published var searchResults: [Video] = [] 13 | @Published var searchStatus: SearchStatus = .idle 14 | @Published var playlistVideos: [Video] = [] 15 | @Published var selectedPlaylist: String = "" 16 | @Published var showingVideoPlayer = false 17 | @Published var currentVideoURL: URL? 18 | @Published var currentVideoTitle: String = "" 19 | 20 | @Dependency(\.searchClient) private var searchClient 21 | @Dependency(\.appStateClient) private var appStateClient 22 | @Dependency(\.floatingWindowClient) private var floatingWindowClient 23 | private var settingsWindowController: NSWindowController? 24 | 25 | private let updaterController: SPUStandardUpdaterController 26 | 27 | enum SearchStatus: Equatable { 28 | case idle 29 | case searching 30 | case completed 31 | case error(String) 32 | } 33 | 34 | init() { 35 | updaterController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil) 36 | 37 | // Configure automatic update checking 38 | @Shared(.autoCheckUpdates) var autoCheckUpdates 39 | updaterController.updater.automaticallyChecksForUpdates = autoCheckUpdates 40 | } 41 | 42 | 43 | func showVideoInApp(_ url: URL, _ title: String) { 44 | // Only show overlay if not popup mode 45 | currentVideoURL = url 46 | currentVideoTitle = title 47 | showingVideoPlayer = true 48 | } 49 | 50 | func hideVideoPlayer() { 51 | showingVideoPlayer = false 52 | currentVideoURL = nil 53 | currentVideoTitle = "" 54 | } 55 | 56 | 57 | // MARK: - Navigation 58 | 59 | func navigateTo(_ page: Pages) { 60 | currentPage = page 61 | } 62 | 63 | // MARK: - Search 64 | 65 | func search(_ query: String) async { 66 | guard !query.isEmpty else { return } 67 | 68 | searchQuery = query 69 | searchStatus = .searching 70 | 71 | @Shared(.apiKey) var apiKey 72 | @Shared(.logs) var logs 73 | 74 | do { 75 | searchResults = try await searchClient.searchVideos(query, apiKey) 76 | searchStatus = .completed 77 | $logs.withLock { $0.append("Search completed: \(searchResults.count) results") } 78 | } catch { 79 | searchStatus = .error(error.localizedDescription) 80 | $logs.withLock { $0.append("Search error: \(error.localizedDescription)") } 81 | } 82 | } 83 | 84 | // MARK: - Playlists 85 | 86 | @Dependency(\.playlistClient) private var playlistClient 87 | @Published var playlistStatus: PlaylistStatus = .idle 88 | 89 | enum PlaylistStatus: Equatable { 90 | case idle 91 | case loading 92 | case completed 93 | case error(String) 94 | } 95 | 96 | func loadPlaylist() async { 97 | @Shared(.apiKey) var apiKey 98 | @Shared(.playlistID) var playlistID 99 | @Shared(.logs) var logs 100 | 101 | playlistStatus = .loading 102 | 103 | do { 104 | let videos = try await playlistClient.fetchVideos(apiKey, playlistID) 105 | playlistVideos = videos 106 | selectedPlaylist = playlistID 107 | playlistStatus = .completed 108 | $logs.withLock { $0.append("PlayList: Loaded \(videos.count) videos") } 109 | } catch { 110 | playlistStatus = .error(error.localizedDescription) 111 | $logs.withLock { $0.append("PlayList Error: \(error.localizedDescription)") } 112 | } 113 | } 114 | 115 | // MARK: - Video Actions 116 | 117 | func handleVideoTap(_ video: Video) async { 118 | @Shared(.videoClickBehaviour) var videoClickBehaviour 119 | 120 | switch videoClickBehaviour { 121 | case .nothing: 122 | return 123 | case .playVideo: 124 | await appStateClient.playVideo(video.url, video.title, false) 125 | case .openOnYoutube: 126 | appStateClient.openInYouTube(video.url) 127 | case .playInIINA: 128 | await appStateClient.playVideo(video.url, video.title, true) 129 | } 130 | } 131 | 132 | func playVideo(_ video: Video) async { 133 | await appStateClient.playVideo(video.url, video.title, false) 134 | } 135 | 136 | func playInIINA(_ video: Video) async { 137 | await appStateClient.playVideo(video.url, video.title, true) 138 | } 139 | 140 | func openInYouTube(_ video: Video) { 141 | appStateClient.openInYouTube(video.url) 142 | } 143 | 144 | func copyVideoLink(_ video: Video) { 145 | let pasteboard = NSPasteboard.general 146 | pasteboard.clearContents() 147 | pasteboard.setString(video.url.absoluteString, forType: .string) 148 | } 149 | 150 | func shareVideo(_ url: URL) { 151 | let sharingPicker = NSSharingServicePicker(items: [url]) 152 | if let window = NSApp.keyWindow { 153 | sharingPicker.show(relativeTo: .zero, of: window.contentView!, preferredEdge: .minY) 154 | } 155 | } 156 | 157 | // MARK: - App Actions 158 | 159 | func quit() { 160 | NSApplication.shared.terminate(nil) 161 | } 162 | 163 | // MARK: - Update Actions 164 | 165 | func checkForUpdates() { 166 | updaterController.checkForUpdates(nil) 167 | } 168 | 169 | func checkForUpdatesInBackground() { 170 | updaterController.updater.checkForUpdatesInBackground() 171 | } 172 | 173 | // MARK: - Settings Window 174 | 175 | func showSettings() { 176 | // If the settings window already exists, just bring it to the front. 177 | if let existingWindow = settingsWindowController?.window { 178 | existingWindow.makeKeyAndOrderFront(nil) 179 | return 180 | } 181 | 182 | // Use a standard titled/closable window so it can become the key window 183 | let styleMask: NSWindow.StyleMask = [.titled, .closable, .fullSizeContentView] 184 | let window = NSWindow( 185 | contentRect: NSRect(x: 0, y: 0, width: 400, height: 600), 186 | styleMask: styleMask, 187 | backing: .buffered, 188 | defer: false 189 | ) 190 | window.titlebarAppearsTransparent = true 191 | window.titleVisibility = .hidden 192 | window.isOpaque = false 193 | window.backgroundColor = .clear 194 | window.hasShadow = true 195 | window.level = .floating 196 | window.isMovableByWindowBackground = true 197 | 198 | let rootView = PreferencesView() 199 | 200 | window.contentView = NSHostingView(rootView: rootView) 201 | 202 | // Position the window and show it. 203 | window.center() 204 | settingsWindowController = NSWindowController(window: window) 205 | settingsWindowController?.showWindow(nil) 206 | 207 | // Clean up when the window closes. 208 | NotificationCenter.default.addObserver( 209 | forName: NSWindow.willCloseNotification, 210 | object: window, 211 | queue: .main 212 | ) { [weak self] _ in 213 | self?.settingsWindowController = nil 214 | } 215 | } 216 | 217 | } 218 | -------------------------------------------------------------------------------- /NativeYoutube/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.388", 9 | "green" : "0.294", 10 | "red" : "0.922" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NativeYoutube/Assets.xcassets/AppIcon.appiconset/AppIcon 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aayush9029/NativeYoutube/3c42cbbd5f8b06a90165b97ae6064d45940482bc/NativeYoutube/Assets.xcassets/AppIcon.appiconset/AppIcon 1.png -------------------------------------------------------------------------------- /NativeYoutube/Assets.xcassets/AppIcon.appiconset/AppIcon 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aayush9029/NativeYoutube/3c42cbbd5f8b06a90165b97ae6064d45940482bc/NativeYoutube/Assets.xcassets/AppIcon.appiconset/AppIcon 2.png -------------------------------------------------------------------------------- /NativeYoutube/Assets.xcassets/AppIcon.appiconset/AppIcon 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aayush9029/NativeYoutube/3c42cbbd5f8b06a90165b97ae6064d45940482bc/NativeYoutube/Assets.xcassets/AppIcon.appiconset/AppIcon 3.png -------------------------------------------------------------------------------- /NativeYoutube/Assets.xcassets/AppIcon.appiconset/AppIcon 4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aayush9029/NativeYoutube/3c42cbbd5f8b06a90165b97ae6064d45940482bc/NativeYoutube/Assets.xcassets/AppIcon.appiconset/AppIcon 4.png -------------------------------------------------------------------------------- /NativeYoutube/Assets.xcassets/AppIcon.appiconset/AppIcon 5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aayush9029/NativeYoutube/3c42cbbd5f8b06a90165b97ae6064d45940482bc/NativeYoutube/Assets.xcassets/AppIcon.appiconset/AppIcon 5.png -------------------------------------------------------------------------------- /NativeYoutube/Assets.xcassets/AppIcon.appiconset/AppIcon 6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aayush9029/NativeYoutube/3c42cbbd5f8b06a90165b97ae6064d45940482bc/NativeYoutube/Assets.xcassets/AppIcon.appiconset/AppIcon 6.png -------------------------------------------------------------------------------- /NativeYoutube/Assets.xcassets/AppIcon.appiconset/AppIcon 7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aayush9029/NativeYoutube/3c42cbbd5f8b06a90165b97ae6064d45940482bc/NativeYoutube/Assets.xcassets/AppIcon.appiconset/AppIcon 7.png -------------------------------------------------------------------------------- /NativeYoutube/Assets.xcassets/AppIcon.appiconset/AppIcon 8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aayush9029/NativeYoutube/3c42cbbd5f8b06a90165b97ae6064d45940482bc/NativeYoutube/Assets.xcassets/AppIcon.appiconset/AppIcon 8.png -------------------------------------------------------------------------------- /NativeYoutube/Assets.xcassets/AppIcon.appiconset/AppIcon 9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aayush9029/NativeYoutube/3c42cbbd5f8b06a90165b97ae6064d45940482bc/NativeYoutube/Assets.xcassets/AppIcon.appiconset/AppIcon 9.png -------------------------------------------------------------------------------- /NativeYoutube/Assets.xcassets/AppIcon.appiconset/AppIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aayush9029/NativeYoutube/3c42cbbd5f8b06a90165b97ae6064d45940482bc/NativeYoutube/Assets.xcassets/AppIcon.appiconset/AppIcon.png -------------------------------------------------------------------------------- /NativeYoutube/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon 9.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "AppIcon 8.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "AppIcon 7.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "AppIcon 6.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "AppIcon 5.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "AppIcon 4.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "AppIcon 3.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "AppIcon 2.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "AppIcon 1.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "AppIcon.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /NativeYoutube/Assets.xcassets/AppIconImage.imageset/AppIcon 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aayush9029/NativeYoutube/3c42cbbd5f8b06a90165b97ae6064d45940482bc/NativeYoutube/Assets.xcassets/AppIconImage.imageset/AppIcon 2.png -------------------------------------------------------------------------------- /NativeYoutube/Assets.xcassets/AppIconImage.imageset/AppIcon 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aayush9029/NativeYoutube/3c42cbbd5f8b06a90165b97ae6064d45940482bc/NativeYoutube/Assets.xcassets/AppIconImage.imageset/AppIcon 3.png -------------------------------------------------------------------------------- /NativeYoutube/Assets.xcassets/AppIconImage.imageset/AppIcon 5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aayush9029/NativeYoutube/3c42cbbd5f8b06a90165b97ae6064d45940482bc/NativeYoutube/Assets.xcassets/AppIconImage.imageset/AppIcon 5.png -------------------------------------------------------------------------------- /NativeYoutube/Assets.xcassets/AppIconImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon 5.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "AppIcon 3.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "AppIcon 2.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /NativeYoutube/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NativeYoutube/Clients/AppStateClient.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import Dependencies 3 | import Foundation 4 | import Shared 5 | import SwiftUI 6 | 7 | @DependencyClient 8 | public struct AppStateClient { 9 | public var playVideo: (_ url: URL, _ title: String, _ useIINA: Bool) async -> Void = { _, _, _ in } 10 | public var stopVideo: () async -> Void = {} 11 | public var openInYouTube: (_ url: URL) -> Void = { _ in } 12 | public var showVideoInApp: @Sendable (_ url: URL, _ title: String) -> Void = { _, _ in } 13 | public var hideVideoPlayer: @Sendable () -> Void = {} 14 | } 15 | 16 | extension AppStateClient: DependencyKey { 17 | public static var liveValue: AppStateClient { 18 | @Dependency(\.floatingWindowClient) var windowClient 19 | 20 | return AppStateClient( 21 | playVideo: { url, title, useIINA in 22 | if useIINA { 23 | // Use mpv to play YouTube videos 24 | await MainActor.run { 25 | // Common locations for IINA's mpv binary 26 | let possibleMpvPaths = [ 27 | "/Applications/IINA.app/Contents/Frameworks/MPVPlayer.framework/Versions/A/Resources/mpv", 28 | "/Applications/IINA.app/Contents/MacOS/mpv", 29 | "/usr/local/bin/mpv" // Fallback to system mpv if available 30 | ] 31 | 32 | // Find the first available mpv binary 33 | let mpvPath = possibleMpvPaths.first { path in 34 | FileManager.default.fileExists(atPath: path) 35 | } 36 | 37 | if let mpvPath = mpvPath { 38 | let task = Process() 39 | task.executableURL = URL(fileURLWithPath: mpvPath) 40 | task.arguments = ["--force-window=yes", "--title=\(title)", url.absoluteString] 41 | 42 | do { 43 | try task.run() 44 | } catch { 45 | print("Failed to launch mpv: \(error)") 46 | // Fallback to IINA URL scheme 47 | let iinaURL = URL(string: "iina://weblink?url=\(url.absoluteString)")! 48 | NSWorkspace.shared.open(iinaURL) 49 | } 50 | } else { 51 | // Fallback to IINA URL scheme if mpv binary not found 52 | let iinaURL = URL(string: "iina://weblink?url=\(url.absoluteString)")! 53 | NSWorkspace.shared.open(iinaURL) 54 | } 55 | } 56 | } else { 57 | // Show in YouTube player window 58 | await MainActor.run { 59 | // Create player view content 60 | let playerView = YouTubePlayerView( 61 | videoURL: url, 62 | title: title 63 | ) 64 | 65 | let hostingView = NSHostingView(rootView: playerView) 66 | 67 | // Create floating panel if it doesn't exist 68 | if !windowClient.isVisible() { 69 | windowClient.createFloatingPanel(hostingView) 70 | } else { 71 | windowClient.updateContent(hostingView) 72 | } 73 | 74 | // Show the panel 75 | windowClient.showPanel() 76 | } 77 | } 78 | }, 79 | stopVideo: { 80 | await MainActor.run { 81 | windowClient.hidePanel() 82 | } 83 | }, 84 | openInYouTube: { url in 85 | NSWorkspace.shared.open(url) 86 | }, 87 | showVideoInApp: { _, _ in 88 | // This method is used to show video in the main app window overlay 89 | // The coordinator handles this directly now via playVideo 90 | }, 91 | hideVideoPlayer: { 92 | Task { @MainActor in 93 | windowClient.hidePanel() 94 | } 95 | } 96 | ) 97 | } 98 | 99 | public static let previewValue = AppStateClient( 100 | playVideo: { url, title, useIINA in 101 | // Mock implementation for previews 102 | print("Preview: Playing video '\(title)' at \(url) \(useIINA ? "with IINA" : "in app")") 103 | }, 104 | stopVideo: { 105 | print("Preview: Stopping video") 106 | }, 107 | openInYouTube: { url in 108 | print("Preview: Opening \(url) in YouTube") 109 | }, 110 | showVideoInApp: { _, title in 111 | print("Preview: Showing video '\(title)' in app") 112 | }, 113 | hideVideoPlayer: { 114 | print("Preview: Hiding video player") 115 | } 116 | ) 117 | 118 | public static let testValue = AppStateClient() 119 | } 120 | 121 | public extension DependencyValues { 122 | var appStateClient: AppStateClient { 123 | get { self[AppStateClient.self] } 124 | set { self[AppStateClient.self] = newValue } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /NativeYoutube/Clients/WindowClient/FloatingPanel.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | class FloatingPanel: NSPanel { 4 | init( 5 | contentRect: NSRect = .zero, 6 | backing: NSWindow.BackingStoreType = .buffered, 7 | defer flag: Bool = false 8 | ) { 9 | super.init( 10 | contentRect: contentRect, 11 | styleMask: [ 12 | .titled, 13 | .resizable, 14 | .closable, 15 | .fullSizeContentView, 16 | .nonactivatingPanel 17 | ], 18 | backing: backing, 19 | defer: flag 20 | ) 21 | self.titlebarAppearsTransparent = true 22 | self.titleVisibility = .hidden 23 | self.isFloatingPanel = true 24 | self.level = .floating 25 | self.collectionBehavior.insert(.fullScreenAuxiliary) 26 | self.titleVisibility = .hidden 27 | self.titlebarAppearsTransparent = true 28 | self.isMovableByWindowBackground = true 29 | self.isReleasedWhenClosed = false 30 | self.standardWindowButton(.closeButton)?.isHidden = true 31 | self.standardWindowButton(.miniaturizeButton)?.isHidden = true 32 | self.standardWindowButton(.zoomButton)?.isHidden = true 33 | 34 | // Set minimum window size 35 | self.minSize = NSSize(width: 400, height: 300) 36 | } 37 | 38 | override var canBecomeKey: Bool { 39 | return true 40 | } 41 | 42 | override var canBecomeMain: Bool { 43 | return true 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /NativeYoutube/Clients/WindowClient/FloatingWindowClient.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import Dependencies 3 | import OSLog 4 | import SwiftUI 5 | 6 | public struct FloatingWindowClient { 7 | public var createFloatingPanel: @MainActor (NSView) -> Void 8 | public var isVisible: @MainActor () -> Bool 9 | public var showPanel: @MainActor () -> Void 10 | public var hidePanel: @MainActor () -> Void 11 | public var centerPanel: @MainActor () -> Void 12 | public var updateContent: @MainActor (NSView) -> Void 13 | public var setCloseHandler: @MainActor (@escaping () -> Void) -> Void 14 | } 15 | 16 | // Helper to hold window state outside of the main implementation 17 | private extension FloatingWindowClient { 18 | @MainActor 19 | final class WindowStateHolder { 20 | var floatingPanel: FloatingPanel? 21 | var closeHandler: (() -> Void)? 22 | static let shared = WindowStateHolder() 23 | } 24 | } 25 | 26 | extension FloatingWindowClient: DependencyKey { 27 | public static let liveValue: Self = { 28 | let state = WindowStateHolder.shared 29 | 30 | return Self( 31 | createFloatingPanel: { contentView in 32 | let panel = FloatingPanel( 33 | contentRect: NSRect(x: 0, y: 0, width: 800, height: 420), 34 | backing: .buffered, 35 | defer: false 36 | ) 37 | panel.titleVisibility = .hidden 38 | panel.backgroundColor = .clear 39 | panel.animationBehavior = .utilityWindow 40 | panel.contentView = contentView 41 | 42 | // Set up default close handler to clean up state 43 | let delegate = PanelDelegate() 44 | panel.delegate = delegate 45 | 46 | state.floatingPanel = panel 47 | center(panel: state.floatingPanel) 48 | }, 49 | 50 | isVisible: { 51 | state.floatingPanel?.isVisible ?? false 52 | }, 53 | 54 | showPanel: { 55 | guard let panel = state.floatingPanel else { return } 56 | panel.makeKeyAndOrderFront(nil) 57 | }, 58 | 59 | hidePanel: { 60 | state.floatingPanel?.orderOut(nil) 61 | }, 62 | 63 | centerPanel: { 64 | center(panel: state.floatingPanel) 65 | }, 66 | 67 | updateContent: { contentView in 68 | guard let panel = state.floatingPanel else { return } 69 | panel.contentView = contentView 70 | }, 71 | 72 | setCloseHandler: { handler in 73 | state.closeHandler = handler 74 | } 75 | ) 76 | 77 | func center(panel: NSPanel?) { 78 | guard let panel, let screen = NSScreen.main else { return } 79 | 80 | let screenRect = screen.visibleFrame 81 | let panelRect = panel.frame 82 | 83 | let newOrigin = NSPoint( 84 | x: screenRect.midX - panelRect.width / 2, 85 | y: screenRect.midY - panelRect.height / 2 86 | ) 87 | 88 | panel.setFrameOrigin(newOrigin) 89 | } 90 | }() 91 | } 92 | 93 | // Panel delegate to handle cleanup 94 | @MainActor 95 | private class PanelDelegate: NSObject, NSWindowDelegate { 96 | func windowWillClose(_ notification: Notification) { 97 | if notification.object is FloatingPanel { 98 | let state = FloatingWindowClient.WindowStateHolder.shared 99 | state.closeHandler?() 100 | state.floatingPanel = nil 101 | state.closeHandler = nil 102 | } 103 | } 104 | } 105 | 106 | extension FloatingWindowClient: TestDependencyKey { 107 | public static let testValue = Self( 108 | createFloatingPanel: unimplemented("\(Self.self).createFloatingPanel"), 109 | isVisible: unimplemented("\(Self.self).isVisible", placeholder: false), 110 | showPanel: unimplemented("\(Self.self).showPanel"), 111 | hidePanel: unimplemented("\(Self.self).hidePanel"), 112 | centerPanel: unimplemented("\(Self.self).centerPanel"), 113 | updateContent: unimplemented("\(Self.self).updateContent"), 114 | setCloseHandler: unimplemented("\(Self.self).setCloseHandler") 115 | ) 116 | 117 | public static let noop = Self( 118 | createFloatingPanel: { _ in }, 119 | isVisible: { false }, 120 | showPanel: {}, 121 | hidePanel: {}, 122 | centerPanel: {}, 123 | updateContent: { _ in }, 124 | setCloseHandler: { _ in } 125 | ) 126 | } 127 | 128 | public extension DependencyValues { 129 | var floatingWindowClient: FloatingWindowClient { 130 | get { self[FloatingWindowClient.self] } 131 | set { self[FloatingWindowClient.self] = newValue } 132 | } 133 | } 134 | 135 | // Keep this for backwards compatibility but it now uses FloatingWindowClient internally 136 | public extension DependencyValues { 137 | var youTubePlayerWindowClient: FloatingWindowClient { 138 | get { self[FloatingWindowClient.self] } 139 | set { self[FloatingWindowClient.self] = newValue } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /NativeYoutube/Clients/YouTubeKitClient.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Shared 3 | import YouTubeKit 4 | 5 | struct YouTubeKitClient { 6 | var extractVideoURL: @Sendable (String) async throws -> URL 7 | var extractHighestQualityVideoURL: @Sendable (String) async throws -> URL 8 | var extractAudioOnlyURL: @Sendable (String) async throws -> URL 9 | var extractStreamInfo: @Sendable (String) async throws -> StreamInfo 10 | } 11 | 12 | struct StreamInfo: Equatable, Sendable { 13 | let url: URL 14 | let quality: String 15 | let fileExtension: String 16 | let hasAudio: Bool 17 | let hasVideo: Bool 18 | } 19 | 20 | extension YouTubeKitClient: TestDependencyKey { 21 | static let testValue = YouTubeKitClient( 22 | extractVideoURL: unimplemented("YouTubeKitClient.extractVideoURL"), 23 | extractHighestQualityVideoURL: unimplemented("YouTubeKitClient.extractHighestQualityVideoURL"), 24 | extractAudioOnlyURL: unimplemented("YouTubeKitClient.extractAudioOnlyURL"), 25 | extractStreamInfo: unimplemented("YouTubeKitClient.extractStreamInfo") 26 | ) 27 | 28 | static let previewValue = YouTubeKitClient( 29 | extractVideoURL: { _ in 30 | URL(string: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4")! 31 | }, 32 | extractHighestQualityVideoURL: { _ in 33 | URL(string: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4")! 34 | }, 35 | extractAudioOnlyURL: { _ in 36 | URL(string: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4")! 37 | }, 38 | extractStreamInfo: { _ in 39 | StreamInfo( 40 | url: URL(string: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4")!, 41 | quality: "1080p", 42 | fileExtension: "mp4", 43 | hasAudio: true, 44 | hasVideo: true 45 | ) 46 | } 47 | ) 48 | } 49 | 50 | extension YouTubeKitClient: DependencyKey { 51 | static let liveValue = YouTubeKitClient( 52 | extractVideoURL: { videoIDOrURL in 53 | let youtube: YouTube 54 | 55 | // Handle both video ID and full URL 56 | if videoIDOrURL.contains("youtube.com") || videoIDOrURL.contains("youtu.be") { 57 | youtube = YouTube(url: URL(string: videoIDOrURL)!) 58 | } else { 59 | youtube = YouTube(videoID: videoIDOrURL) 60 | } 61 | 62 | // Try local extraction first, fall back to remote if needed 63 | let streams = try await youtube.streams 64 | 65 | // Get streams that are natively playable and include both video and audio 66 | guard let stream = streams 67 | .filter({ $0.isNativelyPlayable && $0.includesVideoAndAudioTrack }) 68 | .highestResolutionStream() 69 | else { 70 | throw YouTubeKitError.noSuitableStreamFound 71 | } 72 | 73 | return stream.url 74 | }, 75 | extractHighestQualityVideoURL: { videoIDOrURL in 76 | let youtube: YouTube 77 | 78 | if videoIDOrURL.contains("youtube.com") || videoIDOrURL.contains("youtu.be") { 79 | youtube = YouTube(url: URL(string: videoIDOrURL)!) 80 | } else { 81 | youtube = YouTube(videoID: videoIDOrURL) 82 | } 83 | 84 | let streams = try await youtube.streams 85 | 86 | // Get highest resolution stream with both video and audio 87 | guard let stream = streams 88 | .filter({ $0.includesVideoAndAudioTrack && $0.isNativelyPlayable }) 89 | .highestResolutionStream() 90 | else { 91 | throw YouTubeKitError.noSuitableStreamFound 92 | } 93 | 94 | return stream.url 95 | }, 96 | extractAudioOnlyURL: { videoIDOrURL in 97 | let youtube: YouTube 98 | 99 | if videoIDOrURL.contains("youtube.com") || videoIDOrURL.contains("youtu.be") { 100 | youtube = YouTube(url: URL(string: videoIDOrURL)!) 101 | } else { 102 | youtube = YouTube(videoID: videoIDOrURL) 103 | } 104 | 105 | let streams = try await youtube.streams 106 | 107 | // Get highest quality audio-only stream 108 | guard let stream = streams 109 | .filterAudioOnly() 110 | .filter({ $0.fileExtension == .m4a }) 111 | .highestAudioBitrateStream() 112 | else { 113 | throw YouTubeKitError.noSuitableStreamFound 114 | } 115 | 116 | return stream.url 117 | }, 118 | extractStreamInfo: { videoIDOrURL in 119 | let youtube: YouTube 120 | 121 | if videoIDOrURL.contains("youtube.com") || videoIDOrURL.contains("youtu.be") { 122 | youtube = YouTube(url: URL(string: videoIDOrURL)!) 123 | } else { 124 | youtube = YouTube(videoID: videoIDOrURL) 125 | } 126 | 127 | let streams = try await youtube.streams 128 | 129 | guard let stream = streams 130 | .filter({ $0.isNativelyPlayable && $0.includesVideoAndAudioTrack }) 131 | .highestResolutionStream() 132 | else { 133 | throw YouTubeKitError.noSuitableStreamFound 134 | } 135 | 136 | return StreamInfo( 137 | url: stream.url, 138 | quality: stream.videoResolution?.description ?? "Unknown", 139 | fileExtension: stream.fileExtension.rawValue, 140 | hasAudio: stream.includesAudioTrack, 141 | hasVideo: stream.includesVideoTrack 142 | ) 143 | } 144 | ) 145 | } 146 | 147 | extension DependencyValues { 148 | var youTubeKitClient: YouTubeKitClient { 149 | get { self[YouTubeKitClient.self] } 150 | set { self[YouTubeKitClient.self] = newValue } 151 | } 152 | } 153 | 154 | enum YouTubeKitError: LocalizedError { 155 | case noSuitableStreamFound 156 | case invalidURL 157 | case extractionFailed(String) 158 | 159 | var errorDescription: String? { 160 | switch self { 161 | case .noSuitableStreamFound: 162 | return "No suitable stream found for playback" 163 | case .invalidURL: 164 | return "Invalid YouTube URL or video ID" 165 | case .extractionFailed(let message): 166 | return "Failed to extract video: \(message)" 167 | } 168 | } 169 | } 170 | 171 | -------------------------------------------------------------------------------- /NativeYoutube/ContentView.swift: -------------------------------------------------------------------------------- 1 | import Models 2 | import Shared 3 | import SwiftUI 4 | import UI 5 | 6 | struct ContentView: View { 7 | @EnvironmentObject var coordinator: AppCoordinator 8 | @Shared(.isPlaying) private var isPlaying 9 | @Shared(.currentlyPlaying) private var currentlyPlaying 10 | 11 | var body: some View { 12 | ZStack { 13 | VStack(alignment: .center, spacing: 0) { 14 | switch coordinator.currentPage { 15 | case .playlists: 16 | PlayListView() 17 | case .search: 18 | SearchVideosView() 19 | case .settings: 20 | PreferencesView() // Keep showing the playlists when settings is selected 21 | } 22 | BottomBarView( 23 | currentPage: $coordinator.currentPage, 24 | searchQuery: $coordinator.searchQuery, 25 | isPlaying: isPlaying, 26 | currentlyPlaying: currentlyPlaying, 27 | onSearch: { 28 | coordinator.navigateTo(.search) 29 | Task { 30 | await coordinator.search(coordinator.searchQuery) 31 | } 32 | }, 33 | onQuit: coordinator.quit 34 | ) 35 | } 36 | .frame(width: 360.0) 37 | } 38 | .animation(.easeInOut(duration: 0.2), value: coordinator.showingVideoPlayer) 39 | } 40 | } 41 | 42 | #if DEBUG 43 | #Preview { 44 | ContentView() 45 | .environmentObject(AppCoordinator()) 46 | } 47 | #endif 48 | -------------------------------------------------------------------------------- /NativeYoutube/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleURLTypes 6 | 7 | 8 | CFBundleTypeRole 9 | Viewer 10 | CFBundleURLIconFile 11 | AppIcon 12 | CFBundleURLName 13 | com.aayushpokharel.opensource.NativeYoutube 14 | CFBundleURLSchemes 15 | 16 | nativeyoutube 17 | 18 | 19 | 20 | NSSupportsAutomaticTermination 21 | 22 | NSSupportsSuddenTermination 23 | 24 | SUFeedURL 25 | https://raw.githubusercontent.com/Aayush9029/NativeYoutube/main/sparkle/appcast.xml 26 | SUPublicEDKey 27 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsjiUWapXJYIYAj/U1/WNHTWnyHH3tx5gqF+jYIR9eoUkK82bju6U6rNDRAQY4yLUFdB86aP4Fq9Lx3qOBX6JwI3mhxakYCF1GoMis8YOyhnZkGrXOQ+RgmGSzOaT5MadpJaJ1nOrrgfuEQ16PYLlTk9VUpAX+7KWK4CVlWB3c2JqRXGL+E+HU6PwcjAGmCLNo2ltwTzuibnnyd4vHfy00an0Cwj3gU+TbVrIhGla0ScDxdzpl3tcPNFZmPqNLv/2gRExhOMHnRRuOYMxERxwdcB8FTTdcGllGlnpM2hJwMufOiklQddANpbgDaWAHV4BLC/wbwVZPUIoJAvOrR4gO25I14HhwFdGTWtNQyOkvYBYs8UdSFKMuBiM7rLWgzrUbEwvcsVBP4XFCThTFPCXf+G7fITrD6Fd/rjqjXnPg+kIgog/bW1STKAc75WJHwtfkouPbOpE8TIMf8NmDKsS/7FLoW9/fW2SPuSS7xnNPxlrIwrTz9KhICBRWQt0nPsJFLqfmY3WefngU5W3loRYJnjGSuh4TiCTQvPspF7+vvxulS0+LRzy3yoqN2theWHB3/5fG6igTeYzOU4nAhHVwXq1nQ8bgc9RcYG1KeHnL5F0G4YwQ3KCY+N7r5I/B7191Ada2GWMxX0l8IJg5bk8OxjSlCznofjg/Vn4dsx1ylECAwEAAQ== 28 | 29 | 30 | -------------------------------------------------------------------------------- /NativeYoutube/NativeYoutube.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /NativeYoutube/NativeYoutubeApp.swift: -------------------------------------------------------------------------------- 1 | import APIClient 2 | import Clients 3 | import Dependencies 4 | import Shared 5 | import SwiftUI 6 | 7 | @main 8 | struct NativeYoutubeApp: App { 9 | @StateObject private var coordinator = withDependencies { 10 | $0.apiClient = .liveValue 11 | $0.searchClient = .liveValue 12 | $0.playlistClient = .liveValue 13 | $0.appStateClient = .liveValue 14 | } operation: { 15 | AppCoordinator() 16 | } 17 | 18 | var body: some Scene { 19 | MenuBarExtra("Native Youtube", systemImage: "play.rectangle.fill") { 20 | ContentView() 21 | .environmentObject(coordinator) 22 | .frame(width: 360, height: 512) 23 | } 24 | .menuBarExtraStyle(WindowMenuBarExtraStyle()) 25 | .commands { 26 | CommandGroup(replacing: .appInfo) { 27 | Button("About Native Youtube") { 28 | NSApplication.shared.orderFrontStandardAboutPanel(nil) 29 | } 30 | Divider() 31 | Button("Check for Updates...") { 32 | coordinator.checkForUpdates() 33 | } 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /NativeYoutube/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NativeYoutube/Sharing+Keys.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Models 3 | import Sharing 4 | 5 | // Default values 6 | private enum DefaultValues { 7 | // Note this might expire at any time so generate yours 8 | static let apiKey = "AIzaSyD3NN6IhiVng4iQcNHfZEQy-dlAVqTjq6Q" 9 | static let playlistID = "PLVz-LYNW1HKcil_zzy51Z6ruyNLSJbH7m" 10 | } 11 | 12 | // Persistent storage using FileStorage 13 | extension SharedReaderKey where Self == AppStorageKey.Default { 14 | static var apiKey: Self { 15 | Self[.appStorage("apiKey"), default: DefaultValues.apiKey] 16 | } 17 | 18 | static var playlistID: Self { 19 | Self[.appStorage("playlistID"), default: DefaultValues.playlistID] 20 | } 21 | } 22 | 23 | extension SharedReaderKey where Self == AppStorageKey.Default { 24 | static var useIINA: Self { 25 | Self[.appStorage("useIINA"), default: false] 26 | } 27 | 28 | static var autoCheckUpdates: Self { 29 | Self[.appStorage("autoCheckUpdates"), default: true] 30 | } 31 | } 32 | 33 | extension SharedReaderKey where Self == AppStorageKey.Default { 34 | static var videoClickBehaviour: Self { 35 | Self[.appStorage("videoClickBehaviour"), default: .playVideo] 36 | } 37 | } 38 | 39 | // In-memory keys for runtime state 40 | extension SharedReaderKey where Self == InMemoryKey<[String]>.Default { 41 | static var logs: Self { 42 | Self[.inMemory("logs"), default: []] 43 | } 44 | } 45 | 46 | extension SharedReaderKey where Self == InMemoryKey.Default { 47 | static var isPlaying: Self { 48 | Self[.inMemory("isPlaying"), default: false] 49 | } 50 | } 51 | 52 | extension SharedReaderKey where Self == InMemoryKey.Default { 53 | static var currentlyPlaying: Self { 54 | Self[.inMemory("currentlyPlaying"), default: ""] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /NativeYoutube/Views/PlayListView/PlayListView.swift: -------------------------------------------------------------------------------- 1 | import Clients 2 | import Shared 3 | import SwiftUI 4 | import UI 5 | import Dependencies 6 | 7 | struct PlayListView: View { 8 | @EnvironmentObject var coordinator: AppCoordinator 9 | @Shared(.videoClickBehaviour) private var videoClickBehaviour 10 | 11 | var body: some View { 12 | Group { 13 | switch coordinator.playlistStatus { 14 | case .loading: 15 | ProgressView() 16 | case .error, .idle: 17 | WelcomeView() 18 | case .completed: 19 | if coordinator.playlistVideos.isEmpty { 20 | WelcomeView() 21 | } else { 22 | VideoListView( 23 | videos: coordinator.playlistVideos, 24 | videoClickBehaviour: videoClickBehaviour, 25 | onVideoTap: { video in 26 | Task { 27 | await coordinator.handleVideoTap(video) 28 | } 29 | }, 30 | useIINA: true, 31 | onPlayVideo: { video in 32 | Task { 33 | await coordinator.playVideo(video) 34 | } 35 | }, 36 | onPlayInIINA: { video in 37 | Task { 38 | await coordinator.playInIINA(video) 39 | } 40 | }, 41 | onOpenInYouTube: { video in 42 | coordinator.openInYouTube(video) 43 | }, 44 | onCopyLink: { video in 45 | coordinator.copyVideoLink(video) 46 | }, 47 | onShareLink: { url in 48 | coordinator.shareVideo(url) 49 | } 50 | ) 51 | } 52 | } 53 | } 54 | .onAppear { 55 | Task { 56 | await coordinator.loadPlaylist() 57 | } 58 | } 59 | .onChange(of: Shared(.playlistID).wrappedValue) { _, _ in 60 | Task { 61 | await coordinator.loadPlaylist() 62 | } 63 | } 64 | .frame(maxWidth: .infinity, maxHeight: .infinity) 65 | } 66 | } 67 | 68 | #if DEBUG 69 | #Preview { 70 | PlayListView() 71 | .environmentObject( 72 | withDependencies({ 73 | $0.playlistClient = .previewValue 74 | $0.appStateClient = .previewValue 75 | }, operation: { 76 | AppCoordinator() 77 | }) 78 | ) 79 | } 80 | #endif -------------------------------------------------------------------------------- /NativeYoutube/Views/PreferencesView/GeneralPreferenceView.swift: -------------------------------------------------------------------------------- 1 | import Models 2 | import Shared 3 | import SwiftUI 4 | import UI 5 | 6 | struct GeneralPreferenceView: View { 7 | @Shared(.playlistID) private var playlistID 8 | @Shared(.useIINA) private var useIINA 9 | @Shared(.videoClickBehaviour) private var videoClickBehaviour 10 | @Shared(.autoCheckUpdates) private var autoCheckUpdates 11 | @EnvironmentObject var coordinator: AppCoordinator 12 | 13 | var body: some View { 14 | VStack(alignment: .leading) { 15 | DisclosureGroup { 16 | TextField("Playlist ID", text: Binding($playlistID)) 17 | .textFieldStyle(.plain) 18 | .thinRoundedBG() 19 | } label: { 20 | Label("Custom Playlist ID", systemImage: "music.note.list") 21 | .bold() 22 | } 23 | .thinRoundedBG() 24 | 25 | Divider() 26 | .opacity(0.5) 27 | 28 | DisclosureGroup { 29 | VStack { 30 | SpacedToggle("Use IINA", isOn: Binding($useIINA)) 31 | 32 | Picker("Double Click to", selection: Binding($videoClickBehaviour)) { 33 | ForEach(VideoClickBehaviour.allCases, id: \.self) { behaviour in 34 | if behaviour != .playInIINA || useIINA { 35 | Text(behaviour.rawValue).tag(behaviour) 36 | } 37 | } 38 | } 39 | } 40 | .thinRoundedBG() 41 | } label: { 42 | Label("Player Settings", systemImage: "play.rectangle.on.rectangle.fill") 43 | .bold() 44 | } 45 | .thinRoundedBG() 46 | 47 | Divider() 48 | .opacity(0.5) 49 | 50 | DisclosureGroup { 51 | VStack { 52 | SpacedToggle("Check for updates automatically", isOn: Binding($autoCheckUpdates)) 53 | 54 | Button("Check for Updates Now") { 55 | coordinator.checkForUpdates() 56 | } 57 | .buttonStyle(.borderedProminent) 58 | .controlSize(.small) 59 | } 60 | .thinRoundedBG() 61 | } label: { 62 | Label("Updates", systemImage: "arrow.triangle.2.circlepath") 63 | .bold() 64 | } 65 | .thinRoundedBG() 66 | } 67 | } 68 | } 69 | 70 | struct SpacedToggle: View { 71 | let title: String 72 | @Binding var isOn: Bool 73 | 74 | init(_ title: String, isOn: Binding) { 75 | self.title = title 76 | self._isOn = isOn 77 | } 78 | 79 | var body: some View { 80 | HStack { 81 | Text(title) 82 | Spacer() 83 | Toggle("", isOn: $isOn) 84 | .toggleStyle(.switch) 85 | .labelsHidden() 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /NativeYoutube/Views/PreferencesView/LogPrefrenceView.swift: -------------------------------------------------------------------------------- 1 | import Shared 2 | import SwiftUI 3 | import UI 4 | 5 | struct LogPrefrenceView: View { 6 | @Shared(.logs) private var logs 7 | 8 | var body: some View { 9 | Group { 10 | DisclosureGroup { 11 | VStack(alignment: .leading) { 12 | ScrollView(.vertical, showsIndicators: false) { 13 | VStack(alignment: .leading) { 14 | ForEach(logs, id: \.self) { log in 15 | LogText(text: log, color: .gray) 16 | } 17 | } 18 | } 19 | } 20 | } 21 | label: { 22 | HStack { 23 | Label("Logs", systemImage: "newspaper.fill") 24 | .bold() 25 | .padding(.top, 5) 26 | 27 | Spacer() 28 | 29 | Label("Copy", systemImage: "clipboard.fill") 30 | .labelStyle(.iconOnly) 31 | .thinRoundedBG(padding: 8, material: .thinMaterial) 32 | .clipShape(Circle()) 33 | .onTapGesture { 34 | copyLogsToClipboard(redacted: true) 35 | } 36 | .contextMenu { 37 | VStack { 38 | Button { 39 | copyLogsToClipboard(redacted: false) 40 | } label: { 41 | Label("Copy Raw", systemImage: "key.radiowaves.forward.fill") 42 | } 43 | 44 | Button { 45 | copyLogsToClipboard(redacted: true) 46 | } label: { 47 | Label("Copy Redacted", systemImage: "eyes.inverse") 48 | } 49 | 50 | Button { 51 | $logs.withLock { $0.removeAll() } 52 | } label: { 53 | Label("Clear Logs", systemImage: "trash.fill") 54 | } 55 | } 56 | } 57 | } 58 | } 59 | } 60 | .thinRoundedBG() 61 | } 62 | 63 | private func copyLogsToClipboard(redacted: Bool) { 64 | let pasteboard = NSPasteboard.general 65 | pasteboard.clearContents() 66 | 67 | var logsString = logs.joined(separator: "\n") 68 | 69 | if redacted { 70 | logsString = logsString.replacingOccurrences(of: "AIzaSy[A-Za-z0-9_-]{33}", with: "[[PRIVATE API KEY]]", options: .regularExpression) 71 | } 72 | 73 | pasteboard.setString(logsString, forType: .string) 74 | } 75 | } 76 | 77 | #if DEBUG 78 | #Preview { 79 | LogPrefrenceView() 80 | } 81 | #endif 82 | 83 | private struct LogText: View { 84 | let text: String 85 | let color: Color 86 | 87 | var body: some View { 88 | HStack { 89 | Text(text) 90 | .font(.system(size: 10, design: .monospaced)) 91 | .foregroundStyle(color) 92 | .textSelection(.enabled) 93 | Spacer() 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /NativeYoutube/Views/PreferencesView/PreferencesView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreferencesView.swift 3 | // NativeYoutube 4 | // 5 | // Created by Erik Bautista on 2/4/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct PreferencesView: View { 11 | var body: some View { 12 | ScrollView(.vertical, showsIndicators: false) { 13 | GeneralPreferenceView() 14 | 15 | Divider() 16 | .opacity(0.5) 17 | 18 | YoutubePreferenceView() 19 | 20 | Divider() 21 | .opacity(0.5) 22 | 23 | LogPrefrenceView() 24 | } 25 | } 26 | } 27 | 28 | #if DEBUG 29 | #Preview { 30 | PreferencesView() 31 | } 32 | #endif -------------------------------------------------------------------------------- /NativeYoutube/Views/PreferencesView/YoutubePreferenceView.swift: -------------------------------------------------------------------------------- 1 | import Shared 2 | import SwiftUI 3 | import UI 4 | 5 | struct YoutubePreferenceView: View { 6 | @Shared(.apiKey) private var apiKey 7 | 8 | var body: some View { 9 | VStack(alignment: .leading) { 10 | Group { 11 | DisclosureGroup { 12 | VStack(alignment: .leading) { 13 | TextField("Your Google API Key", text: Binding($apiKey)) 14 | .textFieldStyle(.plain) 15 | .thinRoundedBG() 16 | 17 | Link( 18 | destination: URL(string: "https://www.youtube.com/watch?v=WrFPERZb7uw")!, 19 | label: { 20 | HStack { 21 | Spacer() 22 | Label("How to get Google API Key?", systemImage: "globe") 23 | Spacer() 24 | } 25 | } 26 | ) 27 | } 28 | 29 | } label: { 30 | Label("Your Youtube API Key", systemImage: "person.badge.key.fill") 31 | .bold() 32 | } 33 | } 34 | .thinRoundedBG() 35 | } 36 | } 37 | } 38 | 39 | #if DEBUG 40 | #Preview { 41 | YoutubePreferenceView() 42 | } 43 | #endif 44 | -------------------------------------------------------------------------------- /NativeYoutube/Views/SearchView/SearchVideosView.swift: -------------------------------------------------------------------------------- 1 | import Clients 2 | import Dependencies 3 | import Shared 4 | import SwiftUI 5 | import UI 6 | 7 | struct SearchVideosView: View { 8 | @EnvironmentObject var coordinator: AppCoordinator 9 | @Shared(.videoClickBehaviour) private var videoClickBehaviour 10 | 11 | var body: some View { 12 | VStack { 13 | switch coordinator.searchStatus { 14 | case .idle: 15 | EmptyStateView() 16 | case .searching: 17 | ProgressView("Searching...") 18 | .frame(maxWidth: .infinity, maxHeight: .infinity) 19 | case .completed: 20 | VideoListView( 21 | videos: coordinator.searchResults, 22 | videoClickBehaviour: videoClickBehaviour, 23 | onVideoTap: { video in 24 | Task { 25 | await coordinator.handleVideoTap(video) 26 | } 27 | }, 28 | useIINA: true, 29 | onPlayVideo: { video in 30 | Task { 31 | await coordinator.playVideo(video) 32 | } 33 | }, 34 | onPlayInIINA: { video in 35 | Task { 36 | await coordinator.playInIINA(video) 37 | } 38 | }, 39 | onOpenInYouTube: { video in 40 | coordinator.openInYouTube(video) 41 | }, 42 | onCopyLink: { video in 43 | coordinator.copyVideoLink(video) 44 | }, 45 | onShareLink: { url in 46 | coordinator.shareVideo(url) 47 | } 48 | ) 49 | .frame(maxWidth: .infinity, maxHeight: .infinity) 50 | case .error(let message): 51 | ErrorView(message: message) 52 | } 53 | } 54 | } 55 | } 56 | 57 | struct EmptyStateView: View { 58 | var body: some View { 59 | VStack { 60 | Image(systemName: "magnifyingglass") 61 | .font(.system(size: 48)) 62 | .foregroundStyle(.tertiary) 63 | Text("Search for videos") 64 | .font(.title3) 65 | .foregroundStyle(.secondary) 66 | } 67 | .frame(maxWidth: .infinity, maxHeight: .infinity) 68 | } 69 | } 70 | 71 | private struct ErrorView: View { 72 | let message: String 73 | 74 | var body: some View { 75 | VStack { 76 | Image(systemName: "exclamationmark.triangle") 77 | .font(.system(size: 48)) 78 | .foregroundStyle(.red) 79 | Text("Error") 80 | .font(.title3) 81 | .fontWeight(.semibold) 82 | Text(message) 83 | .font(.caption) 84 | .foregroundStyle(.secondary) 85 | .multilineTextAlignment(.center) 86 | } 87 | .padding() 88 | .frame(maxWidth: .infinity, maxHeight: .infinity) 89 | } 90 | } 91 | 92 | #if DEBUG 93 | #Preview { 94 | SearchVideosView() 95 | .environmentObject( 96 | withDependencies({ 97 | $0.searchClient = .previewValue 98 | $0.appStateClient = .previewValue 99 | }, operation: { 100 | AppCoordinator() 101 | }) 102 | ) 103 | } 104 | #endif 105 | -------------------------------------------------------------------------------- /NativeYoutube/Views/YouTubePlayerView.swift: -------------------------------------------------------------------------------- 1 | import AVKit 2 | import Dependencies 3 | import SwiftUI 4 | import UI 5 | 6 | struct YouTubePlayerView: View { 7 | let videoURL: URL 8 | let title: String 9 | 10 | @State private var player: AVPlayer? = nil 11 | @State private var isLoading = true 12 | @State private var errorMessage: String? = nil 13 | @State private var isHovering = true 14 | @State private var isPlaying = false 15 | 16 | @Dependency(\.youTubeKitClient) private var youTubeKit 17 | @Dependency(\.floatingWindowClient) private var windowClient 18 | 19 | var body: some View { 20 | ZStack { 21 | if let player = player { 22 | VideoPlayer(player: player) 23 | .ignoresSafeArea() 24 | .onAppear { 25 | player.volume = 0.25 26 | player.play() 27 | isPlaying = true 28 | } 29 | 30 | } else if isLoading { 31 | loadingView 32 | } else if let error = errorMessage { 33 | errorView(error) 34 | } 35 | } 36 | .frame(maxWidth: .infinity, maxHeight: .infinity) 37 | .ignoresSafeArea() 38 | .background(VisualEffectView().ignoresSafeArea()) 39 | .overlay(alignment: .topTrailing) { 40 | closeButton 41 | .padding(.trailing) 42 | } 43 | .onHover { hovering in 44 | withAnimation(.easeInOut(duration: 0.2)) { 45 | isHovering = hovering 46 | } 47 | } 48 | .onAppear { 49 | Task { 50 | await extractAndPlayVideo() 51 | } 52 | 53 | // Set up close handler to stop video 54 | windowClient.setCloseHandler { [weak player] in 55 | player?.pause() 56 | player = nil 57 | } 58 | } 59 | } 60 | 61 | // MARK: - Subviews 62 | 63 | private var closeButton: some View { 64 | Button(action: { 65 | player?.pause() 66 | windowClient.hidePanel() 67 | }) { 68 | Image(systemName: "xmark") 69 | .foregroundStyle(.secondary) 70 | .bold() 71 | .padding(8) 72 | .background(VisualEffectView()) 73 | .clipShape(.circle) 74 | } 75 | .buttonStyle(.plain) 76 | .opacity(isHovering ? 1 : 0) 77 | .animation(.easeInOut(duration: 0.2), value: isHovering) 78 | } 79 | 80 | private var loadingView: some View { 81 | LoadingView(title: title) 82 | } 83 | 84 | private func errorView(_ error: String) -> some View { 85 | ErrorView(error: error, 86 | onRetry: { 87 | Task { 88 | await extractAndPlayVideo() 89 | } 90 | }, 91 | onClose: { 92 | windowClient.hidePanel() 93 | }) 94 | } 95 | 96 | // MARK: - Helper methods 97 | 98 | private func extractAndPlayVideo() async { 99 | isLoading = true 100 | errorMessage = nil 101 | 102 | do { 103 | // Use YouTubeKitClient to extract the stream URL 104 | let streamURL = try await youTubeKit.extractVideoURL(videoURL.absoluteString) 105 | 106 | // Create player with extracted URL 107 | await MainActor.run { 108 | self.player = AVPlayer(url: streamURL) 109 | self.player?.play() 110 | self.isPlaying = true 111 | self.isLoading = false 112 | } 113 | 114 | } catch { 115 | await MainActor.run { 116 | self.isLoading = false 117 | self.errorMessage = "Failed to load video: \(error.localizedDescription)" 118 | if self.errorMessage?.contains("outside world") == true { 119 | // This is a test/preview context without proper YouTube access 120 | self.errorMessage = "Cannot play YouTube videos in preview mode" 121 | } 122 | } 123 | } 124 | } 125 | } 126 | 127 | // MARK: - Subview Components 128 | 129 | struct LoadingView: View { 130 | let title: String 131 | 132 | var body: some View { 133 | VStack(spacing: 20) { 134 | ProgressView() 135 | .progressViewStyle(CircularProgressViewStyle()) 136 | VStack { 137 | Text("Loading video...") 138 | .font(.headline) 139 | Text(title) 140 | .font(.subheadline) 141 | .multilineTextAlignment(.center) 142 | .foregroundStyle(.secondary) 143 | } 144 | } 145 | .padding() 146 | .glowEffect(lineWidth: 6, blurRadius: 48) 147 | } 148 | } 149 | 150 | private struct ErrorView: View { 151 | let error: String 152 | let onRetry: () -> Void 153 | let onClose: () -> Void 154 | 155 | var body: some View { 156 | VStack(spacing: 20) { 157 | Image(systemName: "exclamationmark.triangle.fill") 158 | .font(.system(size: 32)) 159 | .foregroundStyle(.yellow) 160 | VStack { 161 | Text("Video Playback Error") 162 | .font(.headline) 163 | Text(error) 164 | .font(.subheadline) 165 | .multilineTextAlignment(.center) 166 | .foregroundStyle(.secondary) 167 | } 168 | HStack { 169 | Button("Retry") { 170 | onRetry() 171 | } 172 | .buttonStyle(.borderedProminent) 173 | .buttonBorderShape(.capsule) 174 | .tint(.blue) 175 | Button("Close") { 176 | onClose() 177 | } 178 | .buttonStyle(.borderedProminent) 179 | .buttonBorderShape(.capsule) 180 | } 181 | } 182 | .padding() 183 | } 184 | } 185 | 186 | #if DEBUG 187 | struct YouTubePlayerView_Previews: PreviewProvider { 188 | static var previews: some View { 189 | Group { 190 | // Main player view 191 | YouTubePlayerView( 192 | videoURL: URL(string: "https://www.youtube.com/watch?v=dQw4w9WgXcQ")!, 193 | title: "Never Gonna Give You Up" 194 | ) 195 | .frame(width: 800, height: 420) 196 | .preferredColorScheme(.dark) 197 | .previewDisplayName("Player View") 198 | 199 | // Loading state 200 | LoadingView(title: "Never Gonna Give You Up") 201 | .frame(width: 800, height: 420) 202 | .background(VisualEffectView()) 203 | .preferredColorScheme(.dark) 204 | .previewDisplayName("Loading State") 205 | 206 | // Error state 207 | ErrorView(error: "Cannot play YouTube videos in preview mode", 208 | onRetry: { 209 | print("Retry tapped") 210 | }, 211 | onClose: { 212 | print("Close tapped") 213 | }) 214 | .frame(width: 800, height: 420) 215 | .background(VisualEffectView()) 216 | .preferredColorScheme(.dark) 217 | .previewDisplayName("Error State") 218 | } 219 | } 220 | } 221 | #endif 222 | -------------------------------------------------------------------------------- /NativeYoutubeKit/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /NativeYoutubeKit/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.10 2 | import PackageDescription 3 | 4 | extension Target.Dependency { 5 | // Internal Libraries 6 | static let ui: Self = "UI" 7 | static let assets: Self = "Assets" 8 | static let models: Self = "Models" 9 | static let apiClient: Self = "APIClient" 10 | static let shared: Self = "Shared" 11 | static let clients: Self = "Clients" 12 | 13 | // External Dependencies 14 | static let swiftDependencies: Self = .product(name: "Dependencies", package: "swift-dependencies") 15 | static let swiftDependenciesMacros: Self = .product(name: "DependenciesMacros", package: "swift-dependencies") 16 | static let identifiedCollections: Self = .product(name: "IdentifiedCollections", package: "swift-identified-collections") 17 | static let swiftSharing: Self = .product(name: "Sharing", package: "swift-sharing") 18 | static let youTubeKit: Self = .product(name: "YouTubeKit", package: "YouTubeKit") 19 | } 20 | 21 | let package = Package( 22 | name: "NativeYoutubeKit", 23 | platforms: [.macOS(.v14)], 24 | products: [ 25 | .library(name: "UI", targets: ["UI"]), 26 | .library(name: "Assets", targets: ["Assets"]), 27 | .library(name: "Models", targets: ["Models"]), 28 | .library(name: "APIClient", targets: ["APIClient"]), 29 | .library(name: "Shared", targets: ["Shared"]), 30 | .library(name: "Clients", targets: ["Clients"]) 31 | ], 32 | dependencies: [ 33 | .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.7.0"), 34 | .package(url: "https://github.com/pointfreeco/swift-identified-collections", from: "1.1.0"), 35 | .package(url: "https://github.com/pointfreeco/swift-sharing.git", from: "2.4.0"), 36 | .package(url: "https://github.com/alexeichhorn/YouTubeKit.git", from: "0.2.7") 37 | ], 38 | targets: [ 39 | .target( 40 | name: "UI", 41 | dependencies: [ 42 | .assets, 43 | .models, 44 | .shared 45 | ] 46 | ), 47 | .target( 48 | name: "Assets", 49 | resources: [.process("Resources")] 50 | ), 51 | .target( 52 | name: "Models", 53 | dependencies: [ 54 | .shared 55 | ] 56 | ), 57 | .target( 58 | name: "APIClient", 59 | dependencies: [ 60 | .models, 61 | .shared, 62 | .swiftDependencies, 63 | .swiftDependenciesMacros 64 | ] 65 | ), 66 | .target( 67 | name: "Shared", 68 | dependencies: [ 69 | .identifiedCollections, 70 | .swiftSharing, 71 | .youTubeKit, 72 | .swiftDependencies, 73 | .swiftDependenciesMacros 74 | ] 75 | ), 76 | .target( 77 | name: "Clients", 78 | dependencies: [ 79 | .apiClient, 80 | .models, 81 | .shared, 82 | .swiftDependencies, 83 | .swiftDependenciesMacros 84 | ] 85 | ) 86 | ] 87 | ) 88 | -------------------------------------------------------------------------------- /NativeYoutubeKit/Sources/APIClient/APIClient.swift: -------------------------------------------------------------------------------- 1 | import Dependencies 2 | import Foundation 3 | import Models 4 | 5 | public struct APIClient { 6 | public var searchVideos: (SearchRequest) async throws -> [Video] 7 | public var fetchPlaylistVideos: (PlaylistRequest) async throws -> [Video] 8 | 9 | public init( 10 | searchVideos: @escaping (SearchRequest) async throws -> [Video] = { _ in [] }, 11 | fetchPlaylistVideos: @escaping (PlaylistRequest) async throws -> [Video] = { _ in [] } 12 | ) { 13 | self.searchVideos = searchVideos 14 | self.fetchPlaylistVideos = fetchPlaylistVideos 15 | } 16 | } 17 | 18 | public struct SearchRequest: Equatable { 19 | public let query: String 20 | public let apiKey: String 21 | public let maxResults: Int 22 | 23 | public init(query: String, apiKey: String, maxResults: Int = 25) { 24 | self.query = query 25 | self.apiKey = apiKey 26 | self.maxResults = maxResults 27 | } 28 | } 29 | 30 | public struct PlaylistRequest: Equatable { 31 | public let playlistId: String 32 | public let apiKey: String 33 | public let maxResults: Int 34 | 35 | public init(playlistId: String, apiKey: String, maxResults: Int = 25) { 36 | self.playlistId = playlistId 37 | self.apiKey = apiKey 38 | self.maxResults = maxResults 39 | } 40 | } 41 | 42 | // Dependency key for swift-dependencies 43 | extension APIClient: DependencyKey { 44 | public static let liveValue = APIClient( 45 | searchVideos: { request in 46 | // Inline the implementation to avoid the linker issue 47 | let query = request.query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" 48 | let urlString = "https://youtube.googleapis.com/youtube/v3/search?part=snippet&q=\(query)&key=\(request.apiKey)&type=video&maxResults=\(request.maxResults)" 49 | 50 | guard let url = URL(string: urlString) else { 51 | throw URLError(.badURL) 52 | } 53 | 54 | let (data, response) = try await URLSession.shared.data(from: url) 55 | 56 | guard let httpResponse = response as? HTTPURLResponse, 57 | httpResponse.statusCode == 200 58 | else { 59 | throw URLError(.badServerResponse) 60 | } 61 | 62 | let searchResponse = try JSONDecoder().decode(YouTubeSearchResponse.self, from: data) 63 | return searchResponse.items.compactMap { $0.toVideo() } 64 | }, 65 | fetchPlaylistVideos: { request in 66 | // Inline the implementation to avoid the linker issue 67 | let urlString = "https://youtube.googleapis.com/youtube/v3/playlistItems?part=snippet%2CcontentDetails%2Cstatus&playlistId=\(request.playlistId)&key=\(request.apiKey)&maxResults=\(request.maxResults)" 68 | 69 | guard let url = URL(string: urlString) else { 70 | throw URLError(.badURL) 71 | } 72 | 73 | let (data, response) = try await URLSession.shared.data(from: url) 74 | 75 | guard let httpResponse = response as? HTTPURLResponse, 76 | httpResponse.statusCode == 200 77 | else { 78 | throw URLError(.badServerResponse) 79 | } 80 | 81 | let playlistResponse = try JSONDecoder().decode(YouTubePlaylistResponse.self, from: data) 82 | return playlistResponse.items.map { $0.toVideo() } 83 | } 84 | ) 85 | 86 | public static let previewValue = APIClient() 87 | public static let testValue = APIClient() 88 | } 89 | 90 | public extension DependencyValues { 91 | var apiClient: APIClient { 92 | get { self[APIClient.self] } 93 | set { self[APIClient.self] = newValue } 94 | } 95 | } 96 | 97 | -------------------------------------------------------------------------------- /NativeYoutubeKit/Sources/Assets/Assets.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftUI 3 | 4 | public enum Assets { 5 | // App Icon 6 | public static let appIcon = Image("AppIconImage", bundle: .module) 7 | } 8 | -------------------------------------------------------------------------------- /NativeYoutubeKit/Sources/Assets/Resources/Assets.xcassets/AppIconImage.imageset/AppIcon 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aayush9029/NativeYoutube/3c42cbbd5f8b06a90165b97ae6064d45940482bc/NativeYoutubeKit/Sources/Assets/Resources/Assets.xcassets/AppIconImage.imageset/AppIcon 2.png -------------------------------------------------------------------------------- /NativeYoutubeKit/Sources/Assets/Resources/Assets.xcassets/AppIconImage.imageset/AppIcon 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aayush9029/NativeYoutube/3c42cbbd5f8b06a90165b97ae6064d45940482bc/NativeYoutubeKit/Sources/Assets/Resources/Assets.xcassets/AppIconImage.imageset/AppIcon 3.png -------------------------------------------------------------------------------- /NativeYoutubeKit/Sources/Assets/Resources/Assets.xcassets/AppIconImage.imageset/AppIcon 5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aayush9029/NativeYoutube/3c42cbbd5f8b06a90165b97ae6064d45940482bc/NativeYoutubeKit/Sources/Assets/Resources/Assets.xcassets/AppIconImage.imageset/AppIcon 5.png -------------------------------------------------------------------------------- /NativeYoutubeKit/Sources/Assets/Resources/Assets.xcassets/AppIconImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon 5.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "AppIcon 3.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "AppIcon 2.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /NativeYoutubeKit/Sources/Assets/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NativeYoutubeKit/Sources/Clients/PlaylistClient.swift: -------------------------------------------------------------------------------- 1 | import APIClient 2 | import Foundation 3 | import Models 4 | import Shared 5 | import Dependencies 6 | 7 | @DependencyClient 8 | public struct PlaylistClient { 9 | public var fetchVideos: (_ apiKey: String, _ playlistId: String) async throws -> [Video] = { _, _ in [] } 10 | } 11 | 12 | extension PlaylistClient: DependencyKey { 13 | public static let liveValue = PlaylistClient( 14 | fetchVideos: { apiKey, playlistId in 15 | @Dependency(\.apiClient) var apiClient 16 | let request = PlaylistRequest(playlistId: playlistId, apiKey: apiKey) 17 | return try await apiClient.fetchPlaylistVideos(request) 18 | } 19 | ) 20 | 21 | public static let previewValue = PlaylistClient( 22 | fetchVideos: { _, _ in 23 | [ 24 | Video( 25 | id: "1", 26 | title: "SwiftUI Tutorial - Building Your First App", 27 | thumbnail: URL(string: "https://i.ytimg.com/vi/abc123/hqdefault.jpg")!, 28 | publishedAt: "2023-12-01T10:00:00Z", 29 | url: URL(string: "https://www.youtube.com/watch?v=abc123")!, 30 | channelTitle: "SwiftUI Academy" 31 | ), 32 | Video( 33 | id: "2", 34 | title: "Advanced Swift Concurrency with async/await", 35 | thumbnail: URL(string: "https://i.ytimg.com/vi/def456/hqdefault.jpg")!, 36 | publishedAt: "2023-11-30T12:00:00Z", 37 | url: URL(string: "https://www.youtube.com/watch?v=def456")!, 38 | channelTitle: "iOS Developer" 39 | ), 40 | Video( 41 | id: "3", 42 | title: "Building a macOS Menu Bar App", 43 | thumbnail: URL(string: "https://i.ytimg.com/vi/ghi789/hqdefault.jpg")!, 44 | publishedAt: "2023-11-29T14:00:00Z", 45 | url: URL(string: "https://www.youtube.com/watch?v=ghi789")!, 46 | channelTitle: "Mac Development" 47 | ) 48 | ] 49 | } 50 | ) 51 | 52 | public static let testValue = PlaylistClient() 53 | } 54 | 55 | public extension DependencyValues { 56 | var playlistClient: PlaylistClient { 57 | get { self[PlaylistClient.self] } 58 | set { self[PlaylistClient.self] = newValue } 59 | } 60 | } -------------------------------------------------------------------------------- /NativeYoutubeKit/Sources/Clients/SearchClient.swift: -------------------------------------------------------------------------------- 1 | import APIClient 2 | import Dependencies 3 | import Foundation 4 | import Models 5 | import Shared 6 | 7 | @DependencyClient 8 | public struct SearchClient { 9 | public var searchVideos: (_ query: String, _ apiKey: String) async throws -> [Video] = { _, _ in [] } 10 | } 11 | 12 | extension SearchClient: DependencyKey { 13 | public static let liveValue = SearchClient( 14 | searchVideos: { query, apiKey in 15 | @Dependency(\.apiClient) var apiClient 16 | let request = SearchRequest(query: query, apiKey: apiKey) 17 | return try await apiClient.searchVideos(request) 18 | } 19 | ) 20 | 21 | public static let previewValue = SearchClient( 22 | searchVideos: { query, _ in 23 | [ 24 | Video( 25 | id: "search1", 26 | title: "Search Result: \(query)", 27 | thumbnail: URL(string: "https://i.ytimg.com/vi/search1/hqdefault.jpg")!, 28 | publishedAt: "2023-12-01T10:00:00Z", 29 | url: URL(string: "https://www.youtube.com/watch?v=search1")!, 30 | channelTitle: "Search Channel 1" 31 | ), 32 | Video( 33 | id: "search2", 34 | title: "Another Result for: \(query)", 35 | thumbnail: URL(string: "https://i.ytimg.com/vi/search2/hqdefault.jpg")!, 36 | publishedAt: "2023-12-01T09:00:00Z", 37 | url: URL(string: "https://www.youtube.com/watch?v=search2")!, 38 | channelTitle: "Search Channel 2" 39 | ), 40 | Video( 41 | id: "search3", 42 | title: "Popular video about \(query)", 43 | thumbnail: URL(string: "https://i.ytimg.com/vi/search3/hqdefault.jpg")!, 44 | publishedAt: "2023-12-01T08:00:00Z", 45 | url: URL(string: "https://www.youtube.com/watch?v=search3")!, 46 | channelTitle: "Popular Creator" 47 | ) 48 | ] 49 | } 50 | ) 51 | 52 | public static let testValue = SearchClient() 53 | } 54 | 55 | public extension DependencyValues { 56 | var searchClient: SearchClient { 57 | get { self[SearchClient.self] } 58 | set { self[SearchClient.self] = newValue } 59 | } 60 | } -------------------------------------------------------------------------------- /NativeYoutubeKit/Sources/Models/Pages.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum Pages: String, CaseIterable { 4 | case playlists = "Playlists" 5 | case search = "Search" 6 | case settings = "Settings" 7 | } -------------------------------------------------------------------------------- /NativeYoutubeKit/Sources/Models/Video.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // Video model with proper Codable conformance 4 | public struct Video: Codable, Equatable, Identifiable { 5 | public let id: String 6 | public let title: String 7 | public let thumbnail: URL 8 | public let publishedAt: String 9 | public let url: URL 10 | public let channelTitle: String 11 | 12 | public init( 13 | id: String, 14 | title: String, 15 | thumbnail: URL, 16 | publishedAt: String, 17 | url: URL, 18 | channelTitle: String 19 | ) { 20 | self.id = id 21 | self.title = title 22 | self.thumbnail = thumbnail 23 | self.publishedAt = publishedAt 24 | self.url = url 25 | self.channelTitle = channelTitle 26 | } 27 | } -------------------------------------------------------------------------------- /NativeYoutubeKit/Sources/Models/VideoClickBehaviour.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum VideoClickBehaviour: String, CaseIterable, Codable { 4 | case nothing = "Do Nothing" 5 | case playVideo = "Play Video" 6 | case openOnYoutube = "Open on Youtube" 7 | case playInIINA = "Play Using IINA" 8 | } -------------------------------------------------------------------------------- /NativeYoutubeKit/Sources/Models/YouTube/PlaylistResponse.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct YouTubePlaylistResponse: Codable, Equatable { 4 | public let kind: String 5 | public let etag: String 6 | public let nextPageToken: String? 7 | public let prevPageToken: String? 8 | public let pageInfo: PageInfo 9 | public let items: [PlaylistItem] 10 | } 11 | 12 | public struct PlaylistItem: Codable, Equatable { 13 | public let kind: String 14 | public let etag: String 15 | public let id: String 16 | public let snippet: PlaylistSnippet 17 | public let contentDetails: ContentDetails 18 | } 19 | 20 | public struct PlaylistSnippet: Codable, Equatable { 21 | public let publishedAt: String 22 | public let channelId: String 23 | public let title: String 24 | public let description: String 25 | public let thumbnails: Thumbnails 26 | public let channelTitle: String 27 | public let playlistId: String 28 | public let position: Int 29 | public let resourceId: ResourceId 30 | public let videoOwnerChannelTitle: String? 31 | public let videoOwnerChannelId: String? 32 | } 33 | 34 | public struct ContentDetails: Codable, Equatable { 35 | public let videoId: String 36 | public let startAt: String? 37 | public let endAt: String? 38 | public let note: String? 39 | public let videoPublishedAt: String? 40 | } 41 | 42 | public struct ResourceId: Codable, Equatable { 43 | public let kind: String 44 | public let videoId: String 45 | } -------------------------------------------------------------------------------- /NativeYoutubeKit/Sources/Models/YouTube/SearchResponse.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct YouTubeSearchResponse: Codable, Equatable { 4 | public let kind: String 5 | public let etag: String 6 | public let nextPageToken: String? 7 | public let prevPageToken: String? 8 | public let regionCode: String? 9 | public let pageInfo: PageInfo 10 | public let items: [SearchItem] 11 | } 12 | 13 | public struct SearchItem: Codable, Equatable { 14 | public let kind: String 15 | public let etag: String 16 | public let id: SearchId 17 | public let snippet: SearchSnippet 18 | } 19 | 20 | public struct SearchId: Codable, Equatable { 21 | public let kind: String 22 | public let videoId: String? 23 | public let channelId: String? 24 | public let playlistId: String? 25 | } 26 | 27 | public struct SearchSnippet: Codable, Equatable { 28 | public let publishedAt: String 29 | public let channelId: String 30 | public let title: String 31 | public let description: String 32 | public let thumbnails: Thumbnails 33 | public let channelTitle: String 34 | public let liveBroadcastContent: String 35 | } -------------------------------------------------------------------------------- /NativeYoutubeKit/Sources/Models/YouTube/SharedModels.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct PageInfo: Codable, Equatable { 4 | public let totalResults: Int 5 | public let resultsPerPage: Int 6 | } 7 | 8 | public struct Thumbnails: Codable, Equatable { 9 | public let `default`: Thumbnail? 10 | public let medium: Thumbnail? 11 | public let high: Thumbnail? 12 | public let standard: Thumbnail? 13 | public let maxres: Thumbnail? 14 | } 15 | 16 | public struct Thumbnail: Codable, Equatable { 17 | public let url: String 18 | public let width: Int 19 | public let height: Int 20 | } -------------------------------------------------------------------------------- /NativeYoutubeKit/Sources/Models/YouTube/VideoTransformers.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Shared 3 | 4 | // Video transformation helpers 5 | public extension SearchItem { 6 | func toVideo() -> Video? { 7 | guard let videoId = id.videoId else { return nil } 8 | let url = URL(string: "https://www.youtube.com/watch?v=\(videoId)")! 9 | let thumbnail = URL(string: snippet.thumbnails.medium?.url ?? "https://via.placeholder.com/140x100")! 10 | let publishedAt = DateConverter.timestampToDate(timestamp: snippet.publishedAt) 11 | 12 | return Video( 13 | id: videoId, 14 | title: snippet.title, 15 | thumbnail: thumbnail, 16 | publishedAt: publishedAt, 17 | url: url, 18 | channelTitle: snippet.channelTitle 19 | ) 20 | } 21 | } 22 | 23 | public extension PlaylistItem { 24 | func toVideo() -> Video { 25 | let videoId = contentDetails.videoId 26 | let url = URL(string: "https://www.youtube.com/watch?v=\(videoId)")! 27 | let thumbnail = URL(string: snippet.thumbnails.medium?.url ?? "https://via.placeholder.com/140x100")! 28 | let publishedAt = DateConverter.timestampToDate(timestamp: snippet.publishedAt) 29 | let channelTitle = snippet.videoOwnerChannelTitle ?? snippet.channelTitle 30 | 31 | return Video( 32 | id: videoId, 33 | title: snippet.title, 34 | thumbnail: thumbnail, 35 | publishedAt: publishedAt, 36 | url: url, 37 | channelTitle: channelTitle 38 | ) 39 | } 40 | } -------------------------------------------------------------------------------- /NativeYoutubeKit/Sources/NativeYoutubeKit/NativeYoutubeKit.swift: -------------------------------------------------------------------------------- 1 | // Empty file for Package target 2 | // Xcode screams if i delete this :( i hate xcode sm 3 | -------------------------------------------------------------------------------- /NativeYoutubeKit/Sources/Shared/DateConverter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct DateConverter { 4 | public static func dateToString(date: Date) -> String { 5 | let calendar = Calendar.current 6 | 7 | if calendar.isDateInToday(date) { 8 | return "Today" 9 | } else if calendar.isDateInYesterday(date) { 10 | return "Yesterday" 11 | } else { 12 | let formatter = DateFormatter() 13 | formatter.dateStyle = .long 14 | return formatter.string(from: date) 15 | } 16 | } 17 | 18 | public static func timestampToDate(timestamp: String) -> String { 19 | let dateStringFormatter = DateFormatter() 20 | dateStringFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" 21 | let date = dateStringFormatter.date(from: timestamp) 22 | 23 | if let date = date { 24 | return dateToString(date: date) 25 | } 26 | 27 | return timestamp 28 | } 29 | } -------------------------------------------------------------------------------- /NativeYoutubeKit/Sources/Shared/Shared.swift: -------------------------------------------------------------------------------- 1 | @_exported import Dependencies 2 | @_exported import DependenciesMacros 3 | @_exported import IdentifiedCollections 4 | @_exported import Sharing 5 | @_exported import YouTubeKit 6 | -------------------------------------------------------------------------------- /NativeYoutubeKit/Sources/UI/Color+Hexstring.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Color+Hexstring.swift 3 | // AppleIntelligenceGlowEffectKit 4 | // 5 | // Created by Adam Różyński on 23/04/2025. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension Color { 11 | init(hex: String) { 12 | let scanner = Scanner(string: hex) 13 | _ = scanner.scanString("#") 14 | 15 | var hexNumber: UInt64 = 0 16 | scanner.scanHexInt64(&hexNumber) 17 | 18 | let r = Double((hexNumber & 0xff0000) >> 16) / 255 19 | let g = Double((hexNumber & 0x00ff00) >> 8) / 255 20 | let b = Double(hexNumber & 0x0000ff) / 255 21 | 22 | self.init(red: r, green: g, blue: b) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /NativeYoutubeKit/Sources/UI/Components/BottomBarView.swift: -------------------------------------------------------------------------------- 1 | import Models 2 | import SwiftUI 3 | 4 | public struct BottomBarView: View { 5 | @Binding var currentPage: Pages 6 | @Binding var searchQuery: String 7 | var isPlaying: Bool 8 | var currentlyPlaying: String 9 | let onSearch: () -> Void 10 | let onQuit: () -> Void 11 | 12 | // For the morphing effect 13 | @Namespace private var searchNamespace 14 | // To auto‐focus the text field 15 | @FocusState private var isSearchFocused: Bool 16 | 17 | public init( 18 | currentPage: Binding, 19 | searchQuery: Binding, 20 | isPlaying: Bool, 21 | currentlyPlaying: String, 22 | onSearch: @escaping () -> Void, 23 | onQuit: @escaping () -> Void 24 | ) { 25 | self._currentPage = currentPage 26 | self._searchQuery = searchQuery 27 | self.isPlaying = isPlaying 28 | self.currentlyPlaying = currentlyPlaying 29 | self.onSearch = onSearch 30 | self.onQuit = onQuit 31 | } 32 | 33 | public var body: some View { 34 | HStack { 35 | // Playlists button 36 | CleanButton(page: .playlists, image: "music.note.list", binded: $currentPage) 37 | 38 | // Search icon <-> search bar morph 39 | ZStack { 40 | if currentPage != .search { 41 | CleanButton(page: .search, image: "magnifyingglass", binded: $currentPage) 42 | } else { 43 | TextField("Search...", text: $searchQuery, onCommit: onSearch) 44 | .textFieldStyle(.plain) 45 | .focused($isSearchFocused) 46 | .padding(.vertical, 6) 47 | .padding(.horizontal, 12) 48 | .background( 49 | RoundedRectangle(cornerRadius: 8) 50 | .fill(.gray.opacity(0.25)) 51 | .matchedGeometryEffect(id: "searchBG", in: searchNamespace) 52 | ) 53 | .overlay( 54 | RoundedRectangle(cornerRadius: 8) 55 | .stroke(.pink.opacity(0.25), 56 | lineWidth: 2) 57 | ) 58 | .onAppear { isSearchFocused = true } 59 | .transition(.opacity.combined(with: .blurReplace).combined(with: .move(edge: .leading))) 60 | } 61 | } 62 | .animation(.spring(response: 0.5, dampingFraction: 0.8), value: currentPage) 63 | 64 | Spacer(minLength: 0) 65 | 66 | // Now playing ticker or settings 67 | if currentPage != .search { 68 | if isPlaying { 69 | ScrollView(.horizontal, showsIndicators: false) { 70 | Text(currentlyPlaying) 71 | .font(.caption) 72 | .foregroundStyle(.secondary) 73 | } 74 | .lineLimit(1) 75 | } 76 | CleanButton(page: .settings, image: "gear", binded: $currentPage) 77 | .contextMenu { 78 | Button("Quit app", systemImage: "power", action: onQuit) 79 | } 80 | } 81 | } 82 | .padding(8) 83 | .background( 84 | RoundedRectangle(cornerRadius: 10) 85 | .fill(.black.opacity(0.75)) 86 | ) 87 | } 88 | } 89 | 90 | #if DEBUG 91 | #Preview { 92 | struct PreviewWrapper: View { 93 | @State private var currentPage: Pages = .playlists 94 | @State private var searchQuery: String = "" 95 | 96 | var body: some View { 97 | VStack { 98 | BottomBarView( 99 | currentPage: $currentPage, 100 | searchQuery: $searchQuery, 101 | isPlaying: true, 102 | currentlyPlaying: "Example Video Title Playing Right Now", 103 | onSearch: { print("Search triggered") }, 104 | onQuit: { print("Quit triggered") } 105 | ) 106 | 107 | BottomBarView( 108 | currentPage: $currentPage, 109 | searchQuery: $searchQuery, 110 | isPlaying: false, 111 | currentlyPlaying: "", 112 | onSearch: { print("Search triggered") }, 113 | onQuit: { print("Quit triggered") } 114 | ) 115 | .padding() 116 | } 117 | } 118 | } 119 | 120 | return PreviewWrapper() 121 | } 122 | #endif 123 | -------------------------------------------------------------------------------- /NativeYoutubeKit/Sources/UI/Components/CleanButton.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Models 3 | 4 | struct CleanButton: View { 5 | let page: Pages 6 | let image: String 7 | @Binding var binded: Pages 8 | 9 | init(page: Pages, image: String, binded: Binding) { 10 | self.page = page 11 | self.image = image 12 | self._binded = binded 13 | } 14 | 15 | public var body: some View { 16 | Button { 17 | binded = page 18 | } label: { 19 | Group { 20 | Label(page.rawValue, systemImage: image) 21 | .labelStyle(.iconOnly) 22 | .font(.callout) 23 | .foregroundStyle(binded == page ? .red : .gray) 24 | .fontWeight(.medium) 25 | } 26 | .thinRoundedBG(padding: 6, radius: 8) 27 | .overlay( 28 | RoundedRectangle(cornerRadius: 8) 29 | .stroke( 30 | binded == page ? .red : .gray.opacity(0.5), 31 | lineWidth: 2 32 | ) 33 | ) 34 | .padding(1) 35 | } 36 | .buttonStyle(.plain) 37 | .animation(.spring, value: binded) 38 | } 39 | } 40 | 41 | #if DEBUG 42 | #Preview { 43 | struct PreviewWrapper: View { 44 | @State private var currentPage: Pages = .playlists 45 | 46 | var body: some View { 47 | HStack(spacing: 20) { 48 | CleanButton( 49 | page: .playlists, 50 | image: "music.note.list", 51 | binded: $currentPage 52 | ) 53 | 54 | CleanButton( 55 | page: .search, 56 | image: "magnifyingglass", 57 | binded: $currentPage 58 | ) 59 | 60 | CleanButton( 61 | page: .settings, 62 | image: "gear", 63 | binded: $currentPage 64 | ) 65 | } 66 | .padding() 67 | } 68 | } 69 | 70 | return PreviewWrapper() 71 | } 72 | #endif 73 | -------------------------------------------------------------------------------- /NativeYoutubeKit/Sources/UI/Components/VideoContextMenuView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Models 3 | 4 | struct VideoContextMenuView: View { 5 | let video: Video 6 | let useIINA: Bool 7 | let onPlayVideo: () -> Void 8 | let onPlayInIINA: () -> Void 9 | let onOpenInYouTube: () -> Void 10 | let onCopyLink: () -> Void 11 | let onShareLink: (URL) -> Void 12 | 13 | init( 14 | video: Video, 15 | useIINA: Bool, 16 | onPlayVideo: @escaping () -> Void, 17 | onPlayInIINA: @escaping () -> Void, 18 | onOpenInYouTube: @escaping () -> Void, 19 | onCopyLink: @escaping () -> Void, 20 | onShareLink: @escaping (URL) -> Void 21 | ) { 22 | self.video = video 23 | self.useIINA = useIINA 24 | self.onPlayVideo = onPlayVideo 25 | self.onPlayInIINA = onPlayInIINA 26 | self.onOpenInYouTube = onOpenInYouTube 27 | self.onCopyLink = onCopyLink 28 | self.onShareLink = onShareLink 29 | } 30 | 31 | public var body: some View { 32 | Group { 33 | if useIINA { 34 | Button(action: onPlayInIINA) { 35 | Label("Play Video in IINA", systemImage: "play.circle") 36 | } 37 | Divider() 38 | } 39 | 40 | Button(action: onPlayVideo) { 41 | Label("Play Video", systemImage: "play.circle") 42 | } 43 | 44 | Divider() 45 | 46 | Button(action: onOpenInYouTube) { 47 | Label("Open in youtube.com", systemImage: "globe") 48 | } 49 | 50 | Divider() 51 | 52 | Button(action: onCopyLink) { 53 | Label("Copy Link", systemImage: "link") 54 | } 55 | 56 | Divider() 57 | 58 | Button(action: { onShareLink(video.url) }) { 59 | Label("Share", systemImage: "square.and.arrow.up") 60 | } 61 | } 62 | } 63 | } 64 | 65 | #if DEBUG 66 | #Preview { 67 | let sampleVideo = Video( 68 | id: "123", 69 | title: "Sample Video Title", 70 | thumbnail: URL(string: "https://via.placeholder.com/140x100")!, 71 | publishedAt: "Today", 72 | url: URL(string: "https://www.youtube.com/watch?v=123")!, 73 | channelTitle: "Sample Channel" 74 | ) 75 | 76 | return VStack(spacing: 20) { 77 | Menu("Video Menu with IINA") { 78 | VideoContextMenuView( 79 | video: sampleVideo, 80 | useIINA: true, 81 | onPlayVideo: { print("Play video") }, 82 | onPlayInIINA: { print("Play in IINA") }, 83 | onOpenInYouTube: { print("Open in YouTube") }, 84 | onCopyLink: { print("Copy link") }, 85 | onShareLink: { url in print("Share link: \(url)") } 86 | ) 87 | } 88 | 89 | Menu("Video Menu without IINA") { 90 | VideoContextMenuView( 91 | video: sampleVideo, 92 | useIINA: false, 93 | onPlayVideo: { print("Play video") }, 94 | onPlayInIINA: { print("Play in IINA") }, 95 | onOpenInYouTube: { print("Open in YouTube") }, 96 | onCopyLink: { print("Copy link") }, 97 | onShareLink: { url in print("Share link: \(url)") } 98 | ) 99 | } 100 | } 101 | .padding() 102 | } 103 | #endif -------------------------------------------------------------------------------- /NativeYoutubeKit/Sources/UI/Components/VideoListView.swift: -------------------------------------------------------------------------------- 1 | import Models 2 | import SwiftUI 3 | 4 | public struct VideoListView: View { 5 | let videos: [Video] 6 | let videoClickBehaviour: VideoClickBehaviour 7 | let onVideoTap: (Video) -> Void 8 | let useIINA: Bool 9 | let onPlayVideo: (Video) -> Void 10 | let onPlayInIINA: (Video) -> Void 11 | let onOpenInYouTube: (Video) -> Void 12 | let onCopyLink: (Video) -> Void 13 | let onShareLink: (URL) -> Void 14 | 15 | public init( 16 | videos: [Video], 17 | videoClickBehaviour: VideoClickBehaviour, 18 | onVideoTap: @escaping (Video) -> Void, 19 | useIINA: Bool = false, 20 | onPlayVideo: @escaping (Video) -> Void = { _ in }, 21 | onPlayInIINA: @escaping (Video) -> Void = { _ in }, 22 | onOpenInYouTube: @escaping (Video) -> Void = { _ in }, 23 | onCopyLink: @escaping (Video) -> Void = { _ in }, 24 | onShareLink: @escaping (URL) -> Void = { _ in } 25 | ) { 26 | self.videos = videos 27 | self.videoClickBehaviour = videoClickBehaviour 28 | self.onVideoTap = onVideoTap 29 | self.useIINA = useIINA 30 | self.onPlayVideo = onPlayVideo 31 | self.onPlayInIINA = onPlayInIINA 32 | self.onOpenInYouTube = onOpenInYouTube 33 | self.onCopyLink = onCopyLink 34 | self.onShareLink = onShareLink 35 | } 36 | 37 | public var body: some View { 38 | Group { 39 | if videos.isEmpty { 40 | VStack { 41 | Image(systemName: "magnifyingglass.circle.fill") 42 | .resizable() 43 | .scaledToFit() 44 | .frame(width: 128) 45 | .padding() 46 | Text("Search for a Video") 47 | .font(.title3) 48 | } 49 | .foregroundStyle(.quaternary) 50 | } else { 51 | ScrollView(.vertical, showsIndicators: false) { 52 | ForEach(videos) { video in 53 | VideoRowView( 54 | video: video, 55 | useIINA: useIINA, 56 | onPlayVideo: { onPlayVideo(video) }, 57 | onPlayInIINA: { onPlayInIINA(video) }, 58 | onOpenInYouTube: { onOpenInYouTube(video) }, 59 | onCopyLink: { onCopyLink(video) }, 60 | onShareLink: onShareLink 61 | ) 62 | .onTapGesture(count: 2) { 63 | onVideoTap(video) 64 | } 65 | } 66 | .padding(6) 67 | } 68 | } 69 | } 70 | } 71 | } 72 | 73 | #if DEBUG 74 | #Preview { 75 | let sampleVideos = [ 76 | Video( 77 | id: "1", 78 | title: "First Video Title - Long title that might wrap to multiple lines", 79 | thumbnail: URL(string: "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBwgHBgkIBwgKCgkLDRYPDQwMDRsUFRAWIB0iIiAdHx8kKDQsJCYxJx8fLT0tMTU3Ojo6Iys/RD84QzQ5OjcBCgoKDQwNGg8PGjclHyU3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3N//AABEIAFMAlAMBIgACEQEDEQH/xAAcAAACAgMBAQAAAAAAAAAAAAAFBgAEAgMHAQj/xAA6EAACAQMDAgMFBQcDBQAAAAABAgMABBEFEiEGMRNBURQiYXGBIzJCkbEHFVKSocHRFmLwJCZyouH/xAAaAQADAQEBAQAAAAAAAAAAAAAAAwQCAQUG/8QAJhEAAgIBAwQCAgMAAAAAAAAAAAECEQMSITEEBRNBIlFCYRQVkf/aAAwDAQACEQMRAD8AvLOSPaVKI8JyFIzQrqLVL7XLqBr/AA4hXCBQFA9aLzRp4ecCgF+pXUV2MQCPWp0MooTpEL73lxx5+VMllbq9qFXs3wpduxi/75zTfpSD2RPpWZM2jTLY+AmduR86ofvG2iba0LZFNN5GDDSncwDxm+dJlshq3CFrrFsjDw1lU/AVSi6k8LX7h3llOUAQen0rBYDtOxSflQ6K1/7gKkcmLPNZjKzriNs3XVvHasbswqq+ckeCfl60LsuudEurlUmKICcZMeBQ4aHb6ldPPfrvVHMcCZ90AdyR6k5+gFbNQ6X0sQbVt41+IGDTFOPs74pNWjod/FatpIvNKMTZGVZeQaWhfa4FysqIB22gilnpGabSdXbSoLmWWyu0f7KQ58N1GQR8wCPypvFwFgfxF7DvRkfuJiMfTAWr9d9Q6VCkSG2k3HkyRkn9aDv1l1PerlLW3C/7YsVds7D95Xj3d3gxg4jWjfsqKuEUAfAUp59KrljVh1O+BNj6z1qOURukSsTjO3Bpl03rLX0RikyY7fczVXWNNt7iFsoFkH3SBVDRcCMwyH7RT+dEs1xuOwLGlKpBi81zWtTcJc3ZKNxtVQBVrRrIsZopcMEQtk981rtoY/EXPej2lRIJpsY95MVnFNye4ZIqPABNuue1SjslipcnmvKssnAT3AaIjPlSzqepRJfxqqkgLyTRe5DQxPkNnHkM0sahGjTxkblcj8QxRFow7Lt7KvteVPYDBotaapNFHGMjacc0v34ZpQWxk4o7a2wktEUrk+Z9KJUaiNM2rWhtQTcJkD1oJclpI1uIgWRvMUr6tpAs3Y+IGjY7sedWtK1h4VjtpZD4BOD8BSZRUuBqdchoyEQ5BKn1pdXWIE13eZJDkCPPxzTPdWdheW7rDfgFh2FINxYRRStGHIKNwwPNYxxt0zUpVudDutIa6tUWMoXUnl0z3OT8vnVbWDcwW9rYwSurMrEuGyePLJolpl80mnxOAzytGrkcAnIz50OupgNQt5GjlGwEYkkBUL5+fBrqv/ClU1aKvR1o5112ut3iRQSMm85IPC9/Pgmvbu4N7ftZRSnapy/PcVY0JTcWN/cw3kcV1csYo88lYweSPmf0qpPFaaTfW8UMEs95IMYUcn4miW6/ZPspBJ57ewRVd9ijhRWr/UWnZ2NdID8aFa3FqKyie7t5Io+wDUs3ln4pJDLXI4FJbsHmaew9vdRXUHiwOGQ9iKCSyCC0kvFPvxsSee9Tpsf9ALIOplBPGaJ/6Pe4td9xqEYjc5ZB+lLjjqTTNymmkwZcdSzQWSXZsz4TLkEEUc/Z51ENaubvEZQRKO570vavavDJ4EEDSWoTaAgJGaJ9F6Xc6XFcy6bAWllXDI/GKfDFjUdXsVOcm9PoL3PXulQ3EsTrNuRipwvmOK9rn+q9P6zDfS+LbAvIS52nI5JqU+o/Yj5DE3UV6SMun8tB9a1Ga6uYpJduUHGBitSrcuo8RNuON1U7uJ5XXMwAHkB3qbHh0Tuhk8iceQ5GJLkeIFTAx3ps0nK2C+JEvJxnFLeitClrkZJB7t2o4upu7wJIV8PP4RTZGYs1dZwIghmjhDbOSvrSzea2Y7Mww2CWkrHaJDgtjzIpv6juYJLAtgkAZNc2nm9okeec9zwP4R6VvFDU9wnKjbcXEt0v2szMwx3NaY5C7AkYA5rCM4kYZzwOay7OcVXSE2dPS0nj0LStRgUES2keR5ZCgEfPIpdvFvtXnFta2pjXvI47Ivmc0+dFTPf9GadZGASDDKzHgKoY4x8f8Va6ihOl9L6hFaRqHEEjs6A8cE8k/DIFS+L5WVeX4V7OAx3Miy+PayyxgsSpViDjyozZdTa5bqJE1GZmB48XEmP5gaGKqxRZ491cmtKnaqIewQk/P/map0r2iW2PrddXepaNDbXdlDczEmOSVSF57glfLj0oHLIMbPDHHoKXreUwSKzDKFgzDOKaX6htrS3Ec+nPvbkMGGCPzqaWNRew1StFWwu2tbxJlTft5waadL6gtr5Sk2lqVY4+9ik797R3Ev2FsEz5kimOw2KkZ2gds8ViUE92ajJoO2+uaTAJbeLTLgSo+AqjIzWxNc8FmYabeKT5BR/mhVltXXjIWAXcvJOBTdc3+nxvl7m1HxMwFKeJGlNitea6kk26XTr4Ej+CvaK6hrGkvMpXUbThccSj41K54F+zvkOZzTSMCNzd6rbm3ZANMaWaeorNdNjPYCn60T6GadDkT2Zg5A97saKoySTRJGyk57ZqmNPiQkHirenWUYvYSvfd5VlyQxI963uvZdNitBjxbnv8EHf+uB+dc+umCpj+9MHV+oC+125aMl4oPsI8dsL3/wDbcaVrt2PZW+QHeqsUdMBeR2y3ZktEhJycYJ+tbWb36zvNOm0S8On3ZzOiI7jGNpZQxX6Zx9KrTPjn0FNTtGDvn7PIGi6WsB2LxB/5hn+9FtetxPoWowL3lt5Af5TVbp14bbSdOhLqrJaRjbn/AGgVYm8O8lkhkkHh9im7G71+lJvcZTPnG5YeAAPxkCqlw+JsA49zH9asakns9y0AORFMyZ+WRXuk6ZNrmtQ6fbH7WVJNvzVCw/qAKZdKxfs8IBQYq7CY57WINGZJY2w245G3y/xVBBKhaKSJ1kRijKRypBwRW+1YW9xG1wGWIsN//j51matGo7MbSumtDEUgRCByQMVhc3cSwMtvIN2PwntRSXp0SQ7YpGUEcEGh+p6Smm20QHLtwTUsJLgdJMX5MnOST8zVR1AojJH8KqSRnng/lT1YtlIipWbLzXld1HKHfQ9PvNZvvYtPVXn2GTazhfdBAPJ+Yq9peiarqV7eWdnADPZkidWYKEOSMZPyNbP2aXMVt1S8u9Y9thPhpGAGcpiugaTrmi+0WN1YtDHP1Cpu7jc4yoSEDB9CDtGPXdSYY1JWOzwlhm4s5XqQksDZPPLBIL2ATxiGUOQp8m9DV6G6aLSL2aC1KukDlJCeQdppkhk0ux6f9sittNluIenY5VWRFIaUZPIHJOcZ8zVV+oLVNK6XbU4tOMepSTDU5fCGUj39sD7qnIyfICuvEkzMdUlaRyZHQR4BHHBqBVZg3mDkY7g126OPTv3lbDqGLQlmOqEaQLQJlrfac78fh2+vGceeK5r1JdPqerzkQ2kFtbSyQW8VrGEUIrkAn+InzNOlkUVudw9PLNLTEFW9nPrmqRrLM0lzMQrTSuSQO2SfgKO6n0TZQSpBb6hPIxUM5ZBjnyx5efnQ6xka0JKD3jj3vTFXTq07MSfP/n9qnyZn+Jfj7bL8hra/AS3jmR5HjiEaiJeMD+ImtUs94Peikik2jlP/AL60tfve5GdhVc+dajfzE7vEkD4xkNwfmKmdt2VLpJJUez6BY3t69zcPKN7l3jUgAk/pQ69086HdrPYM6BgQkoYh1z3UkfD8/wAxVs3c2cg8/CsZppJ4XSX3iwAyTwMHPFOx5ZJ/LgRk7c2tuQFNcMnLIcetU5bhmYjBx5UXaw3n3mrZHp1qpzIrP8M4FPedCP6zKPHQ977R0xae0HLxbogx81U4X+nH0qt1XIsk8KJ2VCT9aEW2om1hSGCMLGgwqjsK03V29xJvbjjFS2tTY7+Blqi1o8Hj36Q744/EIXdIcKMnzNMslrbCBWNnCFkS4AkjaTAaNCQVJchgSPTyNJaSshyKuHV7sqwAhXcpUlYUU4IwRkD0Ne90fcseHDGDfFnjdX2HqMuVzS+gFfoTeznHdz+tSrUkZkdnOMscmpXk5csZTcvtnqY+3ZYwUfoyeNJdqyIrDOcEVh7LbkHMKHOc+73qVKltn0Dim90eC1twciFAc5ztrYsMasWVFDN94471KlFsFGK9GpbaAAqIYwD3G0c1uUBRtUAAdgK8qUXsGlJ7IyqVKlB0lSpUoAlSpUoAlSpUoAlSpUoAlSpUoAlSpUoA/9k=")!, 80 | publishedAt: "Today", 81 | url: URL(string: "https://www.youtube.com/watch?v=1")!, 82 | channelTitle: "Channel One" 83 | ), 84 | Video( 85 | id: "2", 86 | title: "Second Video", 87 | thumbnail: URL(string: "https://i.ytimg.com/vi/0cqp7ZuHZ5M/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBey6QIE0dzUDpXvCpzT5sACYyTvg")!, 88 | publishedAt: "Yesterday", 89 | url: URL(string: "https://www.youtube.com/watch?v=2")!, 90 | channelTitle: "Channel Two" 91 | ), 92 | Video( 93 | id: "3", 94 | title: "Third Video", 95 | thumbnail: URL(string: "https://i.ytimg.com/vi/xkd36cJ6Z78/hq720.jpg?sqp=-oaymwEcCNAFEJQDSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLA4Pu1BaGgAD9aUpmUUz-0tf7Hmuw")!, 96 | publishedAt: "2 days ago", 97 | url: URL(string: "https://www.youtube.com/watch?v=3")!, 98 | channelTitle: "Channel Three" 99 | ) 100 | ] 101 | 102 | VideoListView( 103 | videos: sampleVideos, 104 | videoClickBehaviour: .playVideo, 105 | onVideoTap: { video in 106 | print("Tapped video: \(video.title)") 107 | } 108 | ) 109 | } 110 | #endif 111 | -------------------------------------------------------------------------------- /NativeYoutubeKit/Sources/UI/Components/VideoRowView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Models 3 | 4 | struct VideoRowView: View { 5 | let video: Video 6 | @State private var focused: Bool = false 7 | let useIINA: Bool 8 | let onPlayVideo: () -> Void 9 | let onPlayInIINA: () -> Void 10 | let onOpenInYouTube: () -> Void 11 | let onCopyLink: () -> Void 12 | let onShareLink: (URL) -> Void 13 | 14 | init( 15 | video: Video, 16 | useIINA: Bool = false, 17 | onPlayVideo: @escaping () -> Void = {}, 18 | onPlayInIINA: @escaping () -> Void = {}, 19 | onOpenInYouTube: @escaping () -> Void = {}, 20 | onCopyLink: @escaping () -> Void = {}, 21 | onShareLink: @escaping (URL) -> Void = { _ in } 22 | ) { 23 | self.video = video 24 | self.useIINA = useIINA 25 | self.onPlayVideo = onPlayVideo 26 | self.onPlayInIINA = onPlayInIINA 27 | self.onOpenInYouTube = onOpenInYouTube 28 | self.onCopyLink = onCopyLink 29 | self.onShareLink = onShareLink 30 | } 31 | 32 | public var body: some View { 33 | Group { 34 | ZStack { 35 | AsyncImage(url: video.thumbnail) { image in 36 | image 37 | .resizable() 38 | .overlay { 39 | Rectangle() 40 | .fill(focused ? .ultraThinMaterial : .ultraThickMaterial) 41 | } 42 | } placeholder: { 43 | Rectangle() 44 | .fill(Color.gray.opacity(0.2)) 45 | } 46 | 47 | HStack { 48 | if !focused { 49 | AsyncImage(url: video.thumbnail) { image in 50 | image 51 | .resizable() 52 | .scaledToFill() 53 | .frame(width: 128, height: 72) 54 | .clipped() 55 | } placeholder: { 56 | Rectangle() 57 | .fill(Color.gray.opacity(0.2)) 58 | } 59 | .cornerRadius(5) 60 | .shadow(radius: 6, x: 2) 61 | .padding(.leading, 4) 62 | .padding(.vertical, 2) 63 | .transition(.offset(x: -130)) 64 | } 65 | 66 | VStack(alignment: .leading, spacing: 2) { 67 | Text(video.title) 68 | .foregroundStyle(.primary) 69 | .bold() 70 | .lineLimit(focused ? 3 : 1) 71 | 72 | Text(video.channelTitle) 73 | .foregroundStyle(.secondary) 74 | .font(focused ? .caption : .footnote) 75 | 76 | if !focused { 77 | Text(video.publishedAt) 78 | .font(.caption2) 79 | .foregroundStyle(.tertiary) 80 | .lineLimit(1) 81 | } 82 | } 83 | .padding(.leading, 10) 84 | Spacer() 85 | } 86 | } 87 | .clipped() 88 | .frame(height: 80) 89 | .containerShape(RoundedRectangle(cornerRadius: 5)) 90 | .overlay(RoundedRectangle(cornerRadius: 5) 91 | .stroke(focused ? Color.pink : .gray.opacity(0.25), lineWidth: 2) 92 | .shadow(color: focused ? .pink : .blue.opacity(0), radius: 10) 93 | ) 94 | .onTapGesture(perform: { 95 | focused.toggle() 96 | }) 97 | .animation(.easeInOut, value: focused) 98 | .contextMenu { 99 | VideoContextMenuView( 100 | video: video, 101 | useIINA: useIINA, 102 | onPlayVideo: onPlayVideo, 103 | onPlayInIINA: onPlayInIINA, 104 | onOpenInYouTube: onOpenInYouTube, 105 | onCopyLink: onCopyLink, 106 | onShareLink: onShareLink 107 | ) 108 | } 109 | } 110 | } 111 | } 112 | 113 | #if DEBUG 114 | #Preview { 115 | VideoRowView( 116 | video: Video( 117 | id: "0", 118 | title: "Olivia Rodrigo - good 4 u (Official Video)", 119 | thumbnail: URL(string: "https://i.ytimg.com/vi/gNi_6U5Pm_o/mqdefault.jpg")!, 120 | publishedAt: "Yesterday", 121 | url: URL(string: "https://www.youtube.com/watch?v=gNi_6U5Pm_o")!, 122 | channelTitle: "OliviaRodrigoVEVO" 123 | ) 124 | ) 125 | } 126 | #endif -------------------------------------------------------------------------------- /NativeYoutubeKit/Sources/UI/Components/WelcomeView.swift: -------------------------------------------------------------------------------- 1 | import Assets 2 | import SwiftUI 3 | 4 | public struct WelcomeView: View { 5 | @State private var jump: Bool = false 6 | 7 | public init() {} 8 | 9 | public var body: some View { 10 | VStack { 11 | Spacer() 12 | VStack { 13 | Text("Native Youtube") 14 | .font(.system(size: 64, design: .serif)).bold() 15 | .foregroundStyle(.primary) 16 | Text("Enter your API credentials...") 17 | .foregroundStyle(.tertiary) 18 | } 19 | .multilineTextAlignment(.center) 20 | Spacer() 21 | HStack { 22 | Spacer() 23 | HStack { 24 | Text("Click Gear Icon") 25 | .foregroundStyle(.secondary) 26 | Image(systemName: "arrow.down") 27 | } 28 | .padding(.horizontal, 12) 29 | .padding(4) 30 | .thinRoundedBG(padding: 4) 31 | .shadow(radius: 4) 32 | } 33 | .offset(y: jump ? -20 : -10) 34 | .animation(.spring(response: 0.5).repeatForever(), value: jump) 35 | .onAppear { 36 | jump.toggle() 37 | } 38 | } 39 | .padding(.horizontal) 40 | .background( 41 | Assets.appIcon 42 | .resizable() 43 | .scaledToFit() 44 | .blur(radius: 96) 45 | ) 46 | } 47 | } 48 | 49 | #if DEBUG 50 | #Preview("Welcome View") { 51 | WelcomeView() 52 | .frame(width: 600, height: 400) 53 | } 54 | #endif 55 | -------------------------------------------------------------------------------- /NativeYoutubeKit/Sources/UI/GlowEffect.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GlowEffect.swift 3 | // AppleIntelligenceGlowEffectKit 4 | // 5 | // Created by Adam Różyński on 23/04/2025. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct GlowEffect: View { 11 | @State private var offset: Int = 0 12 | private var gradientStops: [Gradient.Stop] { 13 | generateGradientStops(offset: offset) 14 | } 15 | 16 | private var timer = Timer.publish(every: 0.4, on: .main, in: .common).autoconnect() 17 | var lineWidth: CGFloat 18 | var cornerRadius: CGFloat 19 | var blurRadius: CGFloat 20 | 21 | public init(lineWidth: CGFloat, cornerRadius: CGFloat, blurRadius: CGFloat) { 22 | self.lineWidth = lineWidth 23 | self.cornerRadius = cornerRadius 24 | self.blurRadius = blurRadius 25 | } 26 | 27 | public var body: some View { 28 | ZStack { 29 | BlurredGradientLine(gradientStops: gradientStops, lineWidth: lineWidth / 3, cornerRadius: cornerRadius, blurRadius: blurRadius / 3) 30 | BlurredGradientLine(gradientStops: gradientStops, lineWidth: lineWidth / 2, cornerRadius: cornerRadius, blurRadius: blurRadius / 2) 31 | BlurredGradientLine(gradientStops: gradientStops, lineWidth: lineWidth, cornerRadius: cornerRadius, blurRadius: blurRadius) 32 | BlurredGradientLine(gradientStops: gradientStops, lineWidth: lineWidth, cornerRadius: cornerRadius, blurRadius: blurRadius) 33 | } 34 | .onReceive(timer) { _ in 35 | withAnimation(.linear(duration: 0.6)) { 36 | offset += 1 37 | } 38 | } 39 | } 40 | } 41 | 42 | struct BlurredGradientLine: View { 43 | let gradientStops: [Gradient.Stop] 44 | let lineWidth: CGFloat 45 | let cornerRadius: CGFloat 46 | let blurRadius: CGFloat 47 | 48 | var body: some View { 49 | ZStack { 50 | RoundedRectangle(cornerRadius: cornerRadius) 51 | .strokeBorder( 52 | AngularGradient( 53 | gradient: Gradient(stops: gradientStops), 54 | center: .center 55 | ), 56 | lineWidth: lineWidth 57 | ) 58 | .blur(radius: blurRadius) 59 | } 60 | } 61 | } 62 | 63 | private let baseColors: [Color] = [ 64 | Color(hex: "BC82F3"), 65 | Color(hex: "F5B9EA"), 66 | Color(hex: "8D9FFF"), 67 | Color(hex: "FF6778"), 68 | Color(hex: "FFBA71"), 69 | Color(hex: "C686FF") 70 | ] 71 | 72 | func generateGradientStops(offset: Int) -> [Gradient.Stop] { 73 | let count = baseColors.count 74 | return (0.. some View { 22 | GeometryReader { proxy in 23 | ZStack { 24 | GlowEffect(lineWidth: lineWidth, cornerRadius: cornerRadius, blurRadius: blurRadius) 25 | .frame(width: proxy.size.width + lineWidth, height: proxy.size.height + lineWidth) 26 | content 27 | } 28 | .frame(width: proxy.size.width, height: proxy.size.height) 29 | .clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)) 30 | } 31 | } 32 | } 33 | 34 | public extension View { 35 | func glowEffect(lineWidth: CGFloat = 4, cornerRadius: CGFloat = 8, blurRadius: CGFloat = 8) -> some View { 36 | modifier(GlowEffectViewModifier(lineWidth: lineWidth, cornerRadius: cornerRadius, blurRadius: blurRadius)) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /NativeYoutubeKit/Sources/UI/ViewModifiers/ThinBackground.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct ThinRoundedBackground: ViewModifier { 4 | let padding: CGFloat 5 | let radius: CGFloat 6 | let material: Material 7 | 8 | public func body(content: Content) -> some View { 9 | content 10 | .padding(padding) 11 | .background(material) 12 | .cornerRadius(radius) 13 | } 14 | } 15 | 16 | public extension View { 17 | func thinRoundedBG(padding: CGFloat = 12, radius: CGFloat = 6, material: Material = .ultraThinMaterial) -> ModifiedContent { 18 | return modifier(ThinRoundedBackground(padding: padding, radius: radius, material: material)) 19 | } 20 | } -------------------------------------------------------------------------------- /NativeYoutubeKit/Sources/UI/ViewModifiers/VisualEffectView.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import SwiftUI 3 | 4 | // Visual Effect View for glass background 5 | public struct VisualEffectView: NSViewRepresentable { 6 | let material: NSVisualEffectView.Material 7 | let blendingMode: NSVisualEffectView.BlendingMode 8 | 9 | public init( 10 | material: NSVisualEffectView.Material = .hudWindow, 11 | blendingMode: NSVisualEffectView.BlendingMode = .behindWindow 12 | ) { 13 | self.material = material 14 | self.blendingMode = blendingMode 15 | } 16 | 17 | public func makeNSView(context: Context) -> NSVisualEffectView { 18 | let effectView = NSVisualEffectView() 19 | effectView.material = material 20 | effectView.blendingMode = blendingMode 21 | effectView.state = .active 22 | return effectView 23 | } 24 | 25 | public func updateNSView(_ nsView: NSVisualEffectView, context: Context) { 26 | nsView.material = material 27 | nsView.blendingMode = blendingMode 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![GoIYUndXoAAVHuX](https://github.com/user-attachments/assets/0530149b-6dda-4a20-ac04-18c525e0729a) 2 | 3 | 4 | [![Swift](https://github.com/Aayush9029/NativeYoutube/actions/workflows/swift.yml/badge.svg?branch=main)](https://github.com/Aayush9029/NativeYoutube/actions/workflows/swift.yml) [![Build and Release](https://github.com/Aayush9029/NativeYoutube/actions/workflows/build-and-release.yml/badge.svg)](https://github.com/Aayush9029/NativeYoutube/actions/workflows/build-and-release.yml) 5 | 6 | 7 | 8 | ## Requirements: 9 | - MacOS 14.0 or above 10 | 11 | ## Usage 12 | ### Download the universal binary from the releases tab. 13 | - Open the app *It's a menu bar app* 14 | 15 | - Click on the gear icon > paste your api key, you're done. 16 | 17 | --- 18 | 19 | 20 | **How do I add custom playlist?** 21 | 22 | > 1. Find a playlist you like, eg: https://www.youtube.com/watch?v=dQw4w9WgXcQ&list=PL634F2B56B8C346A2 23 | > 2. Copy the word after "list=" it's `PL634F2B56B8C346A2` in this case 24 | > 3. Paste it in the prefrence window (NativeYoutube > Settings (Command + , ) > Custom Playlist ID) 25 | -------------------------------------------------------------------------------- /sparkle/README.md: -------------------------------------------------------------------------------- 1 | # Sparkle Update System Setup 2 | 3 | This directory contains the configuration for Sparkle auto-updates. 4 | 5 | ## Contents 6 | 7 | - `dsa_priv.pem` - Private key for signing updates (DO NOT COMMIT!) 8 | - `dsa_pub.pem` - Public key for verifying updates 9 | - `appcast.xml` - Update feed that lists available versions 10 | 11 | ## Workflow 12 | 13 | 1. When you create a new release tag (v1.0.0, v1.1.0, etc.) on GitHub: 14 | - GitHub Actions will automatically build the app 15 | - Create a DMG for distribution 16 | - Create a ZIP for Sparkle updates 17 | - Sign the ZIP with the private key 18 | - Update the appcast.xml file 19 | - Create a GitHub release with all files 20 | 21 | 2. The app checks for updates by: 22 | - Reading the feed URL from Info.plist 23 | - Downloading the appcast.xml 24 | - Verifying signatures with the public key 25 | - Downloading and installing updates 26 | 27 | ## Security Setup 28 | 29 | ### GitHub Secrets Required 30 | 31 | Add these secrets to your GitHub repository: 32 | 33 | - `SIGNING_CERTIFICATE_BASE64` - Your Apple Developer ID certificate in base64 34 | - `SIGNING_CERTIFICATE_PASSWORD` - Certificate password 35 | - `DEVELOPMENT_TEAM` - Your Apple Developer Team ID 36 | - `APPLE_ID` - Apple ID for notarization 37 | - `APPLE_ID_PASSWORD` - App-specific password for notarization 38 | 39 | ### Private Key Security 40 | 41 | The private key (`dsa_priv.pem`) must be: 42 | 1. Stored securely 43 | 2. Never committed to the repository 44 | 3. Added as a GitHub secret if needed for CI/CD 45 | 46 | ## Manual Release Process 47 | 48 | If you need to create a release manually: 49 | 50 | ```bash 51 | # Build the app 52 | ./scripts/build_and_sign.sh 53 | 54 | # Sign the update (requires Sparkle tools) 55 | ./bin/sign_update dist/NativeYoutube-1.0.0.zip -f sparkle/dsa_priv.pem 56 | 57 | # Update appcast.xml with the new version info 58 | # Upload the ZIP to your release 59 | # Commit the updated appcast.xml 60 | ``` 61 | 62 | ## Testing Updates 63 | 64 | 1. Build a test version with a lower version number 65 | 2. Run the app 66 | 3. Trigger "Check for Updates" to test the update flow -------------------------------------------------------------------------------- /sparkle/appcast.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Native Youtube 5 | 6 | Version 1.0.0 7 | Initial Release 9 |
    10 |
  • YouTube player integration
  • 11 |
  • Playlist support
  • 12 |
  • Search functionality
  • 13 |
  • Automatic updates via Sparkle
  • 14 |
15 | ]]>
16 | Fri, 17 May 2025 00:00:00 +0000 17 | 1.0.0 18 | 1.0.0 19 | 14.0 20 | 24 |
25 | 26 | Version 3.1 27 | New Features & Improvements 29 |
    30 |
  • Enhanced performance optimizations
  • 31 |
  • Bug fixes and stability improvements
  • 32 |
  • Updated UI components
  • 33 |
34 | ]]>
35 | Fri, 17 May 2025 00:00:00 +0000 36 | 3.1 37 | 3.1 38 | 14.0 39 | 43 |
44 |
45 |
46 | --------------------------------------------------------------------------------