Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions .github/workflows/ios-testflight.yml
Original file line number Diff line number Diff line change
@@ -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" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store</string>
<key>teamID</key>
<string>${APPLE_TEAM_ID}</string>
<key>signingStyle</key>
<string>automatic</string>
<key>uploadSymbols</key>
<true/>
</dict>
</plist>
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
28 changes: 28 additions & 0 deletions docs/ios-testflight.md
Original file line number Diff line number Diff line change
@@ -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.
11 changes: 11 additions & 0 deletions iosApp/ARVIO/ARVIOApp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import SwiftUI

@main
struct ARVIOApp: App {
var body: some Scene {
WindowGroup {
RootView()
.preferredColorScheme(.dark)
}
}
}
26 changes: 26 additions & 0 deletions iosApp/ARVIO/Assets.xcassets/AppIcon.appiconset/Contents.json
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions iosApp/ARVIO/Assets.xcassets/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info": {
"author": "xcode",
"version": 1
}
}
83 changes: 83 additions & 0 deletions iosApp/ARVIO/Components.swift
Original file line number Diff line number Diff line change
@@ -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))
}
}
101 changes: 101 additions & 0 deletions iosApp/ARVIO/HomeView.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Loading
Loading