diff --git a/.github/workflows/ios-testflight.yml b/.github/workflows/ios-testflight.yml new file mode 100644 index 000000000..45b81acdf --- /dev/null +++ b/.github/workflows/ios-testflight.yml @@ -0,0 +1,105 @@ +name: iOS TestFlight + +on: + workflow_dispatch: + +jobs: + testflight: + name: Build and Upload iOS + runs-on: macos-15 + timeout-minutes: 60 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Select Xcode + run: sudo xcode-select -s /Applications/Xcode.app + + - name: Install XcodeGen + run: brew install xcodegen + + - name: Write App Store Connect key + env: + APP_STORE_CONNECT_PRIVATE_KEY: ${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY }} + APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} + run: | + mkdir -p "$RUNNER_TEMP/private_keys" + printf '%s' "$APP_STORE_CONNECT_PRIVATE_KEY" > "$RUNNER_TEMP/private_keys/AuthKey_${APP_STORE_CONNECT_KEY_ID}.p8" + + - name: Generate Xcode project + run: xcodegen generate --spec iosApp/project.yml --project iosApp + + - name: Archive + env: + APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} + APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: | + xcodebuild \ + -project iosApp/ARVIO.xcodeproj \ + -scheme ARVIO \ + -configuration Release \ + -destination "generic/platform=iOS" \ + -archivePath "$RUNNER_TEMP/ARVIO.xcarchive" \ + DEVELOPMENT_TEAM="$APPLE_TEAM_ID" \ + CURRENT_PROJECT_VERSION="$GITHUB_RUN_NUMBER" \ + CODE_SIGN_STYLE=Automatic \ + -allowProvisioningUpdates \ + -authenticationKeyPath "$RUNNER_TEMP/private_keys/AuthKey_${APP_STORE_CONNECT_KEY_ID}.p8" \ + -authenticationKeyID "$APP_STORE_CONNECT_KEY_ID" \ + -authenticationKeyIssuerID "$APP_STORE_CONNECT_ISSUER_ID" \ + clean archive + + - name: Export IPA + env: + APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} + APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: | + cat > "$RUNNER_TEMP/ExportOptions.plist" < + + + + method + app-store + teamID + ${APPLE_TEAM_ID} + signingStyle + automatic + uploadSymbols + + + + PLIST + + xcodebuild \ + -exportArchive \ + -archivePath "$RUNNER_TEMP/ARVIO.xcarchive" \ + -exportPath "$RUNNER_TEMP/export" \ + -exportOptionsPlist "$RUNNER_TEMP/ExportOptions.plist" \ + -allowProvisioningUpdates \ + -authenticationKeyPath "$RUNNER_TEMP/private_keys/AuthKey_${APP_STORE_CONNECT_KEY_ID}.p8" \ + -authenticationKeyID "$APP_STORE_CONNECT_KEY_ID" \ + -authenticationKeyIssuerID "$APP_STORE_CONNECT_ISSUER_ID" + + - name: Upload to TestFlight + env: + APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} + APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} + run: | + xcrun altool \ + --upload-app \ + --type ios \ + --file "$RUNNER_TEMP/export/ARVIO.ipa" \ + --apiKey "$APP_STORE_CONNECT_KEY_ID" \ + --apiIssuer "$APP_STORE_CONNECT_ISSUER_ID" + + - name: Upload IPA artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: ARVIO-iOS-IPA + path: ${{ runner.temp }}/export/*.ipa + if-no-files-found: ignore diff --git a/docs/ios-testflight.md b/docs/ios-testflight.md new file mode 100644 index 000000000..f4167aadf --- /dev/null +++ b/docs/ios-testflight.md @@ -0,0 +1,28 @@ +# iOS TestFlight + +This repo contains an additive iOS SwiftUI shell under `iosApp/`. + +The Android TV APK remains in `app/` and is not coupled to the iOS target. + +## Required GitHub Actions secrets + +- `APP_STORE_CONNECT_KEY_ID` +- `APP_STORE_CONNECT_ISSUER_ID` +- `APP_STORE_CONNECT_PRIVATE_KEY` +- `APPLE_TEAM_ID` +- `IOS_BUNDLE_ID` + +The current workflow uses GitHub's macOS runner and Xcode automatic signing with the App Store Connect API key. +If Apple refuses automatic signing in CI, the workflow will need manual distribution certificate and provisioning profile secrets. + +## Running + +Use **Actions > iOS TestFlight > Run workflow**. + +The workflow: + +1. Installs XcodeGen. +2. Generates `iosApp/ARVIO.xcodeproj`. +3. Archives the app with automatic signing. +4. Exports an App Store IPA. +5. Uploads the IPA to TestFlight. diff --git a/iosApp/ARVIO/ARVIOApp.swift b/iosApp/ARVIO/ARVIOApp.swift new file mode 100644 index 000000000..72b727c13 --- /dev/null +++ b/iosApp/ARVIO/ARVIOApp.swift @@ -0,0 +1,11 @@ +import SwiftUI + +@main +struct ARVIOApp: App { + var body: some Scene { + WindowGroup { + RootView() + .preferredColorScheme(.dark) + } + } +} diff --git a/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Contents.json b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..3f705d0d2 --- /dev/null +++ b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,26 @@ +{ + "images": [ + { "filename": "Icon-20@2x.png", "idiom": "iphone", "scale": "2x", "size": "20x20" }, + { "filename": "Icon-20@3x.png", "idiom": "iphone", "scale": "3x", "size": "20x20" }, + { "filename": "Icon-29@2x.png", "idiom": "iphone", "scale": "2x", "size": "29x29" }, + { "filename": "Icon-29@3x.png", "idiom": "iphone", "scale": "3x", "size": "29x29" }, + { "filename": "Icon-40@2x.png", "idiom": "iphone", "scale": "2x", "size": "40x40" }, + { "filename": "Icon-40@3x.png", "idiom": "iphone", "scale": "3x", "size": "40x40" }, + { "filename": "Icon-60@2x.png", "idiom": "iphone", "scale": "2x", "size": "60x60" }, + { "filename": "Icon-60@3x.png", "idiom": "iphone", "scale": "3x", "size": "60x60" }, + { "filename": "Icon-iPad-20.png", "idiom": "ipad", "scale": "1x", "size": "20x20" }, + { "filename": "Icon-iPad-20@2x.png", "idiom": "ipad", "scale": "2x", "size": "20x20" }, + { "filename": "Icon-iPad-29.png", "idiom": "ipad", "scale": "1x", "size": "29x29" }, + { "filename": "Icon-iPad-29@2x.png", "idiom": "ipad", "scale": "2x", "size": "29x29" }, + { "filename": "Icon-iPad-40.png", "idiom": "ipad", "scale": "1x", "size": "40x40" }, + { "filename": "Icon-iPad-40@2x.png", "idiom": "ipad", "scale": "2x", "size": "40x40" }, + { "filename": "Icon-iPad-76.png", "idiom": "ipad", "scale": "1x", "size": "76x76" }, + { "filename": "Icon-iPad-76@2x.png", "idiom": "ipad", "scale": "2x", "size": "76x76" }, + { "filename": "Icon-iPad-83.5@2x.png", "idiom": "ipad", "scale": "2x", "size": "83.5x83.5" }, + { "filename": "Icon-1024.png", "idiom": "ios-marketing", "scale": "1x", "size": "1024x1024" } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-1024.png b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-1024.png new file mode 100644 index 000000000..fffd58148 Binary files /dev/null and b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-1024.png differ diff --git a/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-20@2x.png b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-20@2x.png new file mode 100644 index 000000000..a49e0b977 Binary files /dev/null and b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-20@2x.png differ diff --git a/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-20@3x.png b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-20@3x.png new file mode 100644 index 000000000..5d5e62e04 Binary files /dev/null and b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-20@3x.png differ diff --git a/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-29@2x.png b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-29@2x.png new file mode 100644 index 000000000..cb3294652 Binary files /dev/null and b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-29@2x.png differ diff --git a/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-29@3x.png b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-29@3x.png new file mode 100644 index 000000000..5d76c8f1c Binary files /dev/null and b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-29@3x.png differ diff --git a/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png new file mode 100644 index 000000000..c6bde252f Binary files /dev/null and b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png differ diff --git a/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png new file mode 100644 index 000000000..8c80d4705 Binary files /dev/null and b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png differ diff --git a/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png new file mode 100644 index 000000000..8c80d4705 Binary files /dev/null and b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png differ diff --git a/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png new file mode 100644 index 000000000..2fb17c7e8 Binary files /dev/null and b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png differ diff --git a/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-iPad-20.png b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-iPad-20.png new file mode 100644 index 000000000..ab6c4a572 Binary files /dev/null and b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-iPad-20.png differ diff --git a/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-iPad-20@2x.png b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-iPad-20@2x.png new file mode 100644 index 000000000..a49e0b977 Binary files /dev/null and b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-iPad-20@2x.png differ diff --git a/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-iPad-29.png b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-iPad-29.png new file mode 100644 index 000000000..2873e9e30 Binary files /dev/null and b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-iPad-29.png differ diff --git a/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-iPad-29@2x.png b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-iPad-29@2x.png new file mode 100644 index 000000000..cb3294652 Binary files /dev/null and b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-iPad-29@2x.png differ diff --git a/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-iPad-40.png b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-iPad-40.png new file mode 100644 index 000000000..a49e0b977 Binary files /dev/null and b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-iPad-40.png differ diff --git a/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-iPad-40@2x.png b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-iPad-40@2x.png new file mode 100644 index 000000000..c6bde252f Binary files /dev/null and b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-iPad-40@2x.png differ diff --git a/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-iPad-76.png b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-iPad-76.png new file mode 100644 index 000000000..17e751031 Binary files /dev/null and b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-iPad-76.png differ diff --git a/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-iPad-76@2x.png b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-iPad-76@2x.png new file mode 100644 index 000000000..59e14bad6 Binary files /dev/null and b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-iPad-76@2x.png differ diff --git a/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-iPad-83.5@2x.png b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-iPad-83.5@2x.png new file mode 100644 index 000000000..f0d8859f7 Binary files /dev/null and b/iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Icon-iPad-83.5@2x.png differ diff --git a/iosApp/ARVIO/Assets.xcassets/Contents.json b/iosApp/ARVIO/Assets.xcassets/Contents.json new file mode 100644 index 000000000..74d6a722c --- /dev/null +++ b/iosApp/ARVIO/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/iosApp/ARVIO/Components.swift b/iosApp/ARVIO/Components.swift new file mode 100644 index 000000000..19c2c5db5 --- /dev/null +++ b/iosApp/ARVIO/Components.swift @@ -0,0 +1,83 @@ +import SwiftUI + +struct PosterBackdrop: View { + let item: MediaItem + + var body: some View { + LinearGradient( + colors: item.palette.map(Color.init(hex:)), + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + .overlay( + RadialGradient(colors: [Color.white.opacity(0.24), Color.clear], center: .topTrailing, startRadius: 10, endRadius: 320) + ) + .overlay(alignment: .center) { + Text(item.title.uppercased()) + .font(.system(size: 27, weight: .bold, design: .serif)) + .foregroundStyle(Color.white.opacity(0.86)) + .multilineTextAlignment(.center) + .padding(18) + } + } +} + +struct MediaCard: View { + let item: MediaItem + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + PosterBackdrop(item: item) + .frame(width: 210, height: 118) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(ArvioTheme.border, lineWidth: 1) + ) + + Text(item.title) + .font(.system(size: 16, weight: .semibold)) + .foregroundStyle(ArvioTheme.textPrimary) + .lineLimit(1) + + Text(item.subtitle) + .font(.system(size: 13, weight: .medium)) + .foregroundStyle(ArvioTheme.textTertiary) + .lineLimit(1) + + if item.progress > 0 { + ProgressView(value: item.progress) + .tint(ArvioTheme.gold) + .frame(width: 210) + } + } + .frame(width: 210, alignment: .leading) + } +} + +struct PrimaryButton: View { + let title: String + + var body: some View { + Text(title) + .font(.system(size: 15, weight: .bold)) + .foregroundStyle(Color.black) + .padding(.horizontal, 18) + .padding(.vertical, 13) + .background(RoundedRectangle(cornerRadius: 8).fill(ArvioTheme.gold)) + } +} + +struct SecondaryButton: View { + let title: String + + var body: some View { + Text(title) + .font(.system(size: 15, weight: .semibold)) + .foregroundStyle(ArvioTheme.textPrimary) + .padding(.horizontal, 18) + .padding(.vertical, 13) + .background(RoundedRectangle(cornerRadius: 8).fill(ArvioTheme.panel)) + .overlay(RoundedRectangle(cornerRadius: 8).stroke(ArvioTheme.border, lineWidth: 1)) + } +} diff --git a/iosApp/ARVIO/HomeView.swift b/iosApp/ARVIO/HomeView.swift new file mode 100644 index 000000000..c337b7f1a --- /dev/null +++ b/iosApp/ARVIO/HomeView.swift @@ -0,0 +1,101 @@ +import SwiftUI + +struct HomeView: View { + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 28) { + HeroSection(item: featuredItems[0]) + MediaRail(title: "Continue Watching", items: continueWatchingItems) + MediaRail(title: "Featured", items: featuredItems) + } + .padding(.horizontal, 28) + .padding(.bottom, 36) + } + } +} + +struct HeroSection: View { + let item: MediaItem + + var body: some View { + ZStack(alignment: .bottomLeading) { + PosterBackdrop(item: item) + .frame(height: 360) + .clipShape(RoundedRectangle(cornerRadius: 8)) + + LinearGradient(colors: [Color.black.opacity(0.0), Color.black.opacity(0.78)], startPoint: .center, endPoint: .bottom) + .clipShape(RoundedRectangle(cornerRadius: 8)) + + VStack(alignment: .leading, spacing: 12) { + Text(item.title) + .font(.system(size: 42, weight: .bold)) + .foregroundStyle(ArvioTheme.textPrimary) + + Text("\(item.subtitle) • \(item.year) • \(item.duration)") + .font(.system(size: 17, weight: .medium)) + .foregroundStyle(ArvioTheme.textSecondary) + + Text("A premium media hub experience for browsing, tracking and continuing your library across screens.") + .font(.system(size: 17)) + .foregroundStyle(ArvioTheme.textSecondary) + .lineLimit(2) + .frame(maxWidth: 580, alignment: .leading) + + HStack(spacing: 12) { + PrimaryButton(title: "Open Details") + SecondaryButton(title: "Add to Watchlist") + } + .padding(.top, 6) + } + .padding(28) + } + } +} + +struct MediaRail: View { + let title: String + let items: [MediaItem] + + var body: some View { + VStack(alignment: .leading, spacing: 14) { + Text(title) + .font(.system(size: 22, weight: .bold)) + .foregroundStyle(ArvioTheme.textPrimary) + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 14) { + ForEach(items) { item in + MediaCard(item: item) + } + } + } + } + } +} + +struct CatalogView: View { + let title: String + let subtitle: String + let items: [MediaItem] + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 18) { + Text(title) + .font(.system(size: 38, weight: .bold)) + .foregroundStyle(ArvioTheme.textPrimary) + Text(subtitle) + .font(.system(size: 17)) + .foregroundStyle(ArvioTheme.textSecondary) + + LazyVGrid(columns: [GridItem(.adaptive(minimum: 210), spacing: 16)], spacing: 16) { + ForEach(items.isEmpty ? featuredItems : items) { item in + MediaCard(item: item) + } + } + .padding(.top, 8) + } + .padding(28) + } + } +} diff --git a/iosApp/ARVIO/Info.plist b/iosApp/ARVIO/Info.plist new file mode 100644 index 000000000..2a7b9ab8c --- /dev/null +++ b/iosApp/ARVIO/Info.plist @@ -0,0 +1,42 @@ + + + + + CFBundleDisplayName + ARVIO + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + LSRequiresIPhoneOS + + UILaunchScreen + + UIColorName + + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/iosApp/ARVIO/Models.swift b/iosApp/ARVIO/Models.swift new file mode 100644 index 000000000..b7c3ebcb7 --- /dev/null +++ b/iosApp/ARVIO/Models.swift @@ -0,0 +1,31 @@ +import Foundation + +enum MediaKind: String { + case movie = "Movie" + case series = "TV Series" +} + +struct MediaItem: Identifiable, Hashable { + let id = UUID() + let title: String + let subtitle: String + let year: String + let duration: String + let rating: String + let kind: MediaKind + let progress: Double + let palette: [String] +} + +let featuredItems: [MediaItem] = [ + MediaItem(title: "Echoes of Dawn", subtitle: "S1 • E6 • New episode", year: "2026", duration: "45m", rating: "8.7", kind: .series, progress: 0.72, palette: ["#d99b4f", "#4b2b19"]), + MediaItem(title: "Neon Paradox", subtitle: "Cyber noir thriller", year: "2025", duration: "2h 10m", rating: "8.2", kind: .movie, progress: 0.0, palette: ["#2276ff", "#141b3c"]), + MediaItem(title: "Beyond the Wilds", subtitle: "S2 • E3", year: "2026", duration: "50m", rating: "8.0", kind: .series, progress: 0.28, palette: ["#9cbf6a", "#1d2d21"]), + MediaItem(title: "The Architect", subtitle: "Premium action cinema", year: "2025", duration: "1h 58m", rating: "7.9", kind: .movie, progress: 0.0, palette: ["#d64a36", "#241112"]) +] + +let continueWatchingItems: [MediaItem] = [ + MediaItem(title: "Night Watch", subtitle: "Resume episode", year: "2026", duration: "47m left", rating: "8.4", kind: .series, progress: 0.41, palette: ["#315f8d", "#081826"]), + MediaItem(title: "Deep Current", subtitle: "S1 • E4", year: "2026", duration: "19m left", rating: "8.1", kind: .series, progress: 0.64, palette: ["#1a9ec0", "#06242c"]), + MediaItem(title: "The Long Orbit", subtitle: "Continue movie", year: "2025", duration: "54m left", rating: "7.8", kind: .movie, progress: 0.52, palette: ["#7961c8", "#18152d"]) +] diff --git a/iosApp/ARVIO/RootView.swift b/iosApp/ARVIO/RootView.swift new file mode 100644 index 000000000..abed166b5 --- /dev/null +++ b/iosApp/ARVIO/RootView.swift @@ -0,0 +1,115 @@ +import SwiftUI + +enum ArvioTab: String, CaseIterable { + case home = "Home" + case movies = "Movies" + case series = "Series" + case watchlist = "Watchlist" + case search = "Search" + case settings = "Settings" +} + +struct RootView: View { + @State private var selectedTab: ArvioTab = .home + + var body: some View { + ZStack { + ArvioTheme.background.ignoresSafeArea() + + LinearGradient( + colors: [Color(hex: "#0b1b2a").opacity(0.65), Color.black.opacity(0.1), Color.black.opacity(0.72)], + startPoint: .topTrailing, + endPoint: .bottom + ) + .ignoresSafeArea() + + VStack(spacing: 0) { + TopNavigation(selectedTab: $selectedTab) + + Group { + switch selectedTab { + case .home: + HomeView() + case .movies: + CatalogView(title: "Movies", subtitle: "Curated cinema and recent releases.", items: featuredItems.filter { $0.kind == .movie }) + case .series: + CatalogView(title: "Series", subtitle: "Continue seasons and discover premium TV.", items: featuredItems.filter { $0.kind == .series }) + case .watchlist: + WatchlistView() + case .search: + SearchView() + case .settings: + SettingsView() + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + } +} + +struct TopNavigation: View { + @Binding var selectedTab: ArvioTab + + var body: some View { + HStack(spacing: 20) { + BrandMark() + + ForEach(ArvioTab.allCases, id: \.self) { tab in + Button { + selectedTab = tab + } label: { + Text(tab.rawValue) + .font(.system(size: 16, weight: selectedTab == tab ? .semibold : .medium)) + .foregroundStyle(selectedTab == tab ? ArvioTheme.textPrimary : ArvioTheme.textSecondary) + .padding(.horizontal, 16) + .padding(.vertical, 10) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(selectedTab == tab ? ArvioTheme.gold.opacity(0.16) : Color.clear) + ) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(selectedTab == tab ? ArvioTheme.gold : Color.clear, lineWidth: 1) + ) + } + .buttonStyle(.plain) + } + + Spacer() + + Text("ARVIO") + .font(.system(size: 15, weight: .semibold)) + .foregroundStyle(ArvioTheme.textSecondary) + } + .padding(.horizontal, 28) + .padding(.top, 14) + .padding(.bottom, 10) + } +} + +struct BrandMark: View { + var body: some View { + ZStack { + Triangle() + .fill(LinearGradient(colors: [ArvioTheme.gold, Color(hex: "#f1c778")], startPoint: .top, endPoint: .bottom)) + .frame(width: 34, height: 42) + Triangle() + .fill(Color.black.opacity(0.26)) + .frame(width: 14, height: 28) + .offset(x: -1) + } + .frame(width: 52) + } +} + +struct Triangle: Shape { + func path(in rect: CGRect) -> Path { + var path = Path() + path.move(to: CGPoint(x: rect.minX, y: rect.minY)) + path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY)) + path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY)) + path.closeSubpath() + return path + } +} diff --git a/iosApp/ARVIO/SearchView.swift b/iosApp/ARVIO/SearchView.swift new file mode 100644 index 000000000..87abb0912 --- /dev/null +++ b/iosApp/ARVIO/SearchView.swift @@ -0,0 +1,25 @@ +import SwiftUI + +struct SearchView: View { + @State private var query = "" + + var body: some View { + VStack(alignment: .leading, spacing: 22) { + Text("Search") + .font(.system(size: 38, weight: .bold)) + .foregroundStyle(ArvioTheme.textPrimary) + + TextField("Search movies, shows and episodes", text: $query) + .textInputAutocapitalization(.never) + .padding(16) + .background(RoundedRectangle(cornerRadius: 8).fill(ArvioTheme.panel)) + .overlay(RoundedRectangle(cornerRadius: 8).stroke(ArvioTheme.border, lineWidth: 1)) + .foregroundStyle(ArvioTheme.textPrimary) + + MediaRail(title: query.isEmpty ? "Suggested" : "Results", items: featuredItems) + + Spacer() + } + .padding(28) + } +} diff --git a/iosApp/ARVIO/SettingsView.swift b/iosApp/ARVIO/SettingsView.swift new file mode 100644 index 000000000..98b3fb92d --- /dev/null +++ b/iosApp/ARVIO/SettingsView.swift @@ -0,0 +1,39 @@ +import SwiftUI + +struct SettingsView: View { + var body: some View { + VStack(alignment: .leading, spacing: 18) { + Text("Settings") + .font(.system(size: 38, weight: .bold)) + .foregroundStyle(ArvioTheme.textPrimary) + + SettingsRow(title: "Profile", value: "Default") + SettingsRow(title: "Cloud sync", value: "Ready") + SettingsRow(title: "Trakt", value: "Connect later") + SettingsRow(title: "Playback", value: "Native iOS player") + + Spacer() + } + .padding(28) + } +} + +struct SettingsRow: View { + let title: String + let value: String + + var body: some View { + HStack { + Text(title) + .font(.system(size: 17, weight: .semibold)) + .foregroundStyle(ArvioTheme.textPrimary) + Spacer() + Text(value) + .font(.system(size: 15, weight: .medium)) + .foregroundStyle(ArvioTheme.textSecondary) + } + .padding(18) + .background(RoundedRectangle(cornerRadius: 8).fill(ArvioTheme.panel)) + .overlay(RoundedRectangle(cornerRadius: 8).stroke(ArvioTheme.border, lineWidth: 1)) + } +} diff --git a/iosApp/ARVIO/Theme.swift b/iosApp/ARVIO/Theme.swift new file mode 100644 index 000000000..35104daf2 --- /dev/null +++ b/iosApp/ARVIO/Theme.swift @@ -0,0 +1,23 @@ +import SwiftUI + +enum ArvioTheme { + static let background = Color(red: 0.031, green: 0.035, blue: 0.039) + static let panel = Color.white.opacity(0.075) + static let border = Color.white.opacity(0.12) + static let textPrimary = Color(red: 0.93, green: 0.93, blue: 0.93) + static let textSecondary = Color.white.opacity(0.68) + static let textTertiary = Color.white.opacity(0.44) + static let gold = Color(red: 1.0, green: 0.72, blue: 0.24) +} + +extension Color { + init(hex: String) { + let raw = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) + var value: UInt64 = 0 + Scanner(string: raw).scanHexInt64(&value) + let red = Double((value >> 16) & 0xff) / 255 + let green = Double((value >> 8) & 0xff) / 255 + let blue = Double(value & 0xff) / 255 + self.init(red: red, green: green, blue: blue) + } +} diff --git a/iosApp/ARVIO/WatchlistView.swift b/iosApp/ARVIO/WatchlistView.swift new file mode 100644 index 000000000..6a35535cf --- /dev/null +++ b/iosApp/ARVIO/WatchlistView.swift @@ -0,0 +1,11 @@ +import SwiftUI + +struct WatchlistView: View { + var body: some View { + CatalogView( + title: "Watchlist", + subtitle: "Movies and shows saved for later.", + items: featuredItems + continueWatchingItems + ) + } +} diff --git a/iosApp/project.yml b/iosApp/project.yml new file mode 100644 index 000000000..d889e0825 --- /dev/null +++ b/iosApp/project.yml @@ -0,0 +1,28 @@ +name: ARVIO +options: + minimumXcodeGenVersion: 2.40.0 + deploymentTarget: + iOS: "17.0" +settings: + base: + SWIFT_VERSION: 5.10 + IPHONEOS_DEPLOYMENT_TARGET: "17.0" + DEVELOPMENT_TEAM: YSKCRJKAF3 + CODE_SIGN_STYLE: Automatic + TARGETED_DEVICE_FAMILY: "1,2" +targets: + ARVIO: + type: application + platform: iOS + sources: + - path: ARVIO + settings: + base: + PRODUCT_BUNDLE_IDENTIFIER: com.arvio.ios + PRODUCT_NAME: ARVIO + INFOPLIST_FILE: ARVIO/Info.plist + ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon + MARKETING_VERSION: 0.1.0 + CURRENT_PROJECT_VERSION: 1 + GENERATE_INFOPLIST_FILE: NO + ENABLE_USER_SCRIPT_SANDBOXING: NO