Prechádzať zdrojové kódy

Initial GMPlayer project

flyzto 1 týždeň pred
commit
b22856743e
100 zmenil súbory, kde vykonal 1549 pridanie a 0 odobranie
  1. 17 0
      .gitignore
  2. 227 0
      AGENTS.md
  3. 42 0
      README.md
  4. 31 0
      android/app/build.gradle.kts
  5. 22 0
      android/app/src/main/AndroidManifest.xml
  6. 342 0
      android/app/src/main/java/com/gmplayer/app/MainActivity.kt
  7. 60 0
      android/app/src/main/java/com/gmplayer/app/MobileMainControllerButton.kt
  8. 131 0
      android/app/src/main/java/com/gmplayer/app/MobilePlaybackControls.kt
  9. 125 0
      android/app/src/main/java/com/gmplayer/app/MobileProgressBarInfo.kt
  10. 5 0
      android/app/src/main/java/com/gmplayer/app/PlayerControlsConfiguration.kt
  11. BIN
      android/app/src/main/res/drawable-mdpi/player_button_go_backwards_15.png
  12. BIN
      android/app/src/main/res/drawable-mdpi/player_button_go_forward_15.png
  13. BIN
      android/app/src/main/res/drawable-mdpi/player_button_pause.png
  14. BIN
      android/app/src/main/res/drawable-mdpi/player_button_play.png
  15. BIN
      android/app/src/main/res/drawable-mdpi/player_button_replay.png
  16. BIN
      android/app/src/main/res/drawable-mdpi/player_button_skip_backward.png
  17. BIN
      android/app/src/main/res/drawable-mdpi/player_button_skip_forward.png
  18. BIN
      android/app/src/main/res/drawable-nodpi/player_button_go_backwards_15.png
  19. BIN
      android/app/src/main/res/drawable-nodpi/player_button_go_forward_15.png
  20. BIN
      android/app/src/main/res/drawable-nodpi/player_button_pause.png
  21. BIN
      android/app/src/main/res/drawable-nodpi/player_button_play.png
  22. BIN
      android/app/src/main/res/drawable-nodpi/player_button_replay.png
  23. BIN
      android/app/src/main/res/drawable-nodpi/player_button_skip_backward.png
  24. BIN
      android/app/src/main/res/drawable-nodpi/player_button_skip_forward.png
  25. BIN
      android/app/src/main/res/drawable-xhdpi/player_button_go_backwards_15.png
  26. BIN
      android/app/src/main/res/drawable-xhdpi/player_button_go_forward_15.png
  27. BIN
      android/app/src/main/res/drawable-xhdpi/player_button_pause.png
  28. BIN
      android/app/src/main/res/drawable-xhdpi/player_button_play.png
  29. BIN
      android/app/src/main/res/drawable-xhdpi/player_button_replay.png
  30. BIN
      android/app/src/main/res/drawable-xhdpi/player_button_skip_backward.png
  31. BIN
      android/app/src/main/res/drawable-xhdpi/player_button_skip_forward.png
  32. BIN
      android/app/src/main/res/drawable-xxxhdpi/player_button_go_backwards_15.png
  33. BIN
      android/app/src/main/res/drawable-xxxhdpi/player_button_go_forward_15.png
  34. BIN
      android/app/src/main/res/drawable-xxxhdpi/player_button_pause.png
  35. BIN
      android/app/src/main/res/drawable-xxxhdpi/player_button_play.png
  36. BIN
      android/app/src/main/res/drawable-xxxhdpi/player_button_replay.png
  37. BIN
      android/app/src/main/res/drawable-xxxhdpi/player_button_skip_backward.png
  38. BIN
      android/app/src/main/res/drawable-xxxhdpi/player_button_skip_forward.png
  39. 13 0
      android/app/src/main/res/drawable/ic_launcher.xml
  40. BIN
      android/app/src/main/res/mipmap-hdpi/ic_launcher.png
  41. BIN
      android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
  42. BIN
      android/app/src/main/res/mipmap-mdpi/ic_launcher.png
  43. BIN
      android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
  44. BIN
      android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
  45. BIN
      android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
  46. BIN
      android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  47. BIN
      android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
  48. BIN
      android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  49. BIN
      android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
  50. 8 0
      android/app/src/main/res/values/styles.xml
  51. 5 0
      android/build.gradle.kts
  52. 19 0
      android/gradle/libs.versions.toml
  53. 18 0
      android/settings.gradle.kts
  54. BIN
      design-assets/player-buttons/player_button_go_backwards_15.png
  55. BIN
      design-assets/player-buttons/player_button_go_backwards_15@2x.png
  56. BIN
      design-assets/player-buttons/player_button_go_backwards_15@3x.png
  57. BIN
      design-assets/player-buttons/player_button_go_forward_15.png
  58. BIN
      design-assets/player-buttons/player_button_go_forward_15@2x.png
  59. BIN
      design-assets/player-buttons/player_button_go_forward_15@3x.png
  60. BIN
      design-assets/player-buttons/player_button_pause.png
  61. BIN
      design-assets/player-buttons/player_button_pause@2x.png
  62. BIN
      design-assets/player-buttons/player_button_pause@3x.png
  63. BIN
      design-assets/player-buttons/player_button_play.png
  64. BIN
      design-assets/player-buttons/player_button_play@2x.png
  65. BIN
      design-assets/player-buttons/player_button_play@3x.png
  66. BIN
      design-assets/player-buttons/player_button_replay.png
  67. BIN
      design-assets/player-buttons/player_button_replay@2x.png
  68. BIN
      design-assets/player-buttons/player_button_replay@3x.png
  69. BIN
      design-assets/player-buttons/player_button_skip_backward.png
  70. BIN
      design-assets/player-buttons/player_button_skip_backward@2x.png
  71. BIN
      design-assets/player-buttons/player_button_skip_backward@3x.png
  72. BIN
      design-assets/player-buttons/player_button_skip_forward.png
  73. BIN
      design-assets/player-buttons/player_button_skip_forward@2x.png
  74. BIN
      design-assets/player-buttons/player_button_skip_forward@3x.png
  75. BIN
      docs/app-icon-master.png
  76. 55 0
      docs/requirements.md
  77. 271 0
      ios/GMPlayer.xcodeproj/project.pbxproj
  78. 19 0
      ios/GMPlayer/AppDelegate.swift
  79. 116 0
      ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Contents.json
  80. BIN
      ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-1024.png
  81. BIN
      ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-20@2x.png
  82. BIN
      ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-20@2x~ipad.png
  83. BIN
      ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-20@3x.png
  84. BIN
      ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-20~ipad.png
  85. BIN
      ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-29@2x.png
  86. BIN
      ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-29@2x~ipad.png
  87. BIN
      ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-29@3x.png
  88. BIN
      ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-29~ipad.png
  89. BIN
      ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png
  90. BIN
      ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-40@2x~ipad.png
  91. BIN
      ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png
  92. BIN
      ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-40~ipad.png
  93. BIN
      ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png
  94. BIN
      ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png
  95. BIN
      ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-76.png
  96. BIN
      ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png
  97. BIN
      ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png
  98. 23 0
      ios/GMPlayer/Assets.xcassets/player_button_go_backwards_15.imageset/Contents.json
  99. BIN
      ios/GMPlayer/Assets.xcassets/player_button_go_backwards_15.imageset/player_button_go_backwards_15@1x.png
  100. BIN
      ios/GMPlayer/Assets.xcassets/player_button_go_backwards_15.imageset/player_button_go_backwards_15@2x.png

+ 17 - 0
.gitignore

@@ -0,0 +1,17 @@
+.DS_Store
+.build/
+DerivedData/
+
+# Xcode
+*.xcuserstate
+xcuserdata/
+
+# Android
+android/.gradle/
+android/build/
+android/app/build/
+android/local.properties
+
+# IDE
+.idea/
+*.iml

+ 227 - 0
AGENTS.md

@@ -0,0 +1,227 @@
+# GMPlayer Handoff Notes
+
+## Current Project State
+
+GMPlayer is a native mobile app workspace for collecting internet video URLs and playing them directly. The project currently has:
+
+- iOS app in Swift + UIKit.
+- Android app in Kotlin + Jetpack Compose.
+- Shared requirements in `docs/requirements.md`.
+- Design/exported player button assets in `design-assets/player-buttons`.
+
+The root directory is not currently a Git repository in this environment, so use file inspection rather than Git history for handoff context.
+
+## Product Flow
+
+Current main flow:
+
+1. User enters an `http` or `https` video URL.
+2. URL must end with `.m3u8` or `.mp4`.
+3. Valid URLs are added to a local in-memory history list, newest first, max 3 items.
+4. Tapping play/add opens a full-screen player view.
+
+## iOS Status
+
+Main files:
+
+- `ios/GMPlayer/PlayerViewController.swift`
+  - Main URL input and history screen.
+  - Validates `.m3u8` and `.mp4`.
+  - Presents `VideoPlayerViewController(url:)`.
+
+- `ios/GMPlayer/VideoPlayerViewController.swift`
+  - Full-screen custom player based on `AVPlayer` and `AVPlayerLayer`.
+  - Uses custom controls, not `AVPlayerViewController`.
+  - Plays automatically on appear.
+  - Supports close, play/pause/replay, seek backward/forward 15 seconds, progress display, and drag-to-seek.
+  - Control overlay behavior:
+    - Tap empty video area to show/hide controls.
+    - When visible, controls auto-hide after 4 seconds without interaction.
+    - Show/hide uses fade animation.
+    - Playback buttons and progress dragging reset the auto-hide timer.
+
+- `ios/GMPlayer/MobilePlaybackControlsView.swift`
+  - Center control group.
+  - Supports side controls mode:
+    - `.skip15`
+    - `.playlist`
+    - `.hidden`
+
+- `ios/GMPlayer/PlayerControlsConfiguration.swift`
+  - Current side button mode is configured here.
+
+- `ios/GMPlayer/MobileProgressBarInfoView.swift`
+  - Bottom title/time/progress area.
+  - Height expected to be 106.
+  - Progress bar supports horizontal scrubbing.
+
+- `ios/GMPlayer/MobileMainControllerButton.swift`
+- `ios/GMPlayer/MobileSkipButton.swift`
+  - Custom button rendering using exported PNG resources.
+  - Circular background uses `UIVisualEffectView` with `.systemUltraThinMaterialDark`.
+  - No additional black overlay on iOS.
+
+Temporary native system player testing was removed. There should be no `SystemVideoPlayerViewController.swift`.
+
+### iOS Assets
+
+Player button assets live in:
+
+- `ios/GMPlayer/Assets.xcassets/player_button_play.imageset`
+- `ios/GMPlayer/Assets.xcassets/player_button_pause.imageset`
+- `ios/GMPlayer/Assets.xcassets/player_button_replay.imageset`
+- `ios/GMPlayer/Assets.xcassets/player_button_go_backwards_15.imageset`
+- `ios/GMPlayer/Assets.xcassets/player_button_go_forward_15.imageset`
+- `ios/GMPlayer/Assets.xcassets/player_button_skip_backward.imageset`
+- `ios/GMPlayer/Assets.xcassets/player_button_skip_forward.imageset`
+
+App icon assets are in:
+
+- `ios/GMPlayer/Assets.xcassets/AppIcon.appiconset`
+
+### iOS Build And Run
+
+Known working simulator:
+
+- iPhone 17
+- iOS 26.5
+- UDID: `68D65FB5-95B9-4E38-8552-2A1934459005`
+
+Build:
+
+```sh
+xcodebuild -project ios/GMPlayer.xcodeproj -scheme GMPlayer -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.5' build
+```
+
+Install and launch:
+
+```sh
+open -a Simulator
+xcrun simctl boot 68D65FB5-95B9-4E38-8552-2A1934459005
+xcrun simctl install 68D65FB5-95B9-4E38-8552-2A1934459005 /Users/flyzto/Library/Developer/Xcode/DerivedData/GMPlayer-bikaarnrdbqjvfbzwmpbnythjtrj/Build/Products/Debug-iphonesimulator/GMPlayer.app
+xcrun simctl launch 68D65FB5-95B9-4E38-8552-2A1934459005 com.gmplayer.app
+```
+
+Last verified iOS run:
+
+- Date: 2026-06-10
+- Build succeeded.
+- Installed and launched on iPhone 17 / iOS 26.5.
+
+Known warning:
+
+- `PlayerViewController.swift` uses deprecated `UIButton.contentEdgeInsets`; currently harmless for this scaffold.
+
+## Android Status
+
+Main files:
+
+- `android/app/src/main/java/com/gmplayer/app/MainActivity.kt`
+  - Main URL input and history screen.
+  - Opens `AndroidPlayerScreen` after a valid URL is added.
+  - `AndroidPlayerScreen` uses Android `VideoView` with Compose overlays.
+  - Control overlay behavior is synchronized with iOS intent:
+    - Tap video area to show/hide controls.
+    - Auto-hide after 4 seconds when visible.
+    - Fade in/out animation is 220ms.
+    - Button interactions reset the timer.
+    - Android back exits the player screen.
+
+- `android/app/src/main/java/com/gmplayer/app/MobilePlaybackControls.kt`
+  - Center control group.
+  - Supports side controls mode:
+    - `Skip15`
+    - `Playlist`
+    - `Hidden`
+  - Android button backgrounds include a 25% black circular layer so transparent exported icons remain visible.
+
+- `android/app/src/main/java/com/gmplayer/app/MobileMainControllerButton.kt`
+- `android/app/src/main/java/com/gmplayer/app/MobileProgressBarInfo.kt`
+- `android/app/src/main/java/com/gmplayer/app/PlayerControlsConfiguration.kt`
+
+### Android Assets
+
+Button resources were copied from `design-assets/player-buttons` into Android resource folders. Current button names include:
+
+- `player_button_play`
+- `player_button_pause`
+- `player_button_replay`
+- `player_button_go_backwards_15`
+- `player_button_go_forward_15`
+- `player_button_skip_backward`
+- `player_button_skip_forward`
+
+### Android Build Notes
+
+This environment currently does not have:
+
+- `android/gradlew`
+- global `gradle`
+
+So Android has not been compiled locally after the latest player overlay changes. Open `android/` in Android Studio or add/sync a Gradle wrapper before verifying:
+
+```sh
+cd android
+./gradlew :app:assembleDebug
+```
+
+## Player UX Details
+
+iOS currently has the most complete custom player implementation:
+
+- Custom full-screen player.
+- Center play/pause/replay and side controls.
+- Bottom progress info area.
+- Drag-to-seek.
+- Auto-hide controls.
+- Fade transitions.
+
+Android has the matching overlay behavior at the Compose level, but playback is currently implemented with `VideoView`. If parity matters for HLS behavior and richer controls, consider moving Android playback to Media3 ExoPlayer.
+
+## Design/Figma Context
+
+The player UI was based on the Figma file:
+
+- `Video Player For Web Mobile Community`
+- File key: `ApvSwPRpW7jYhJ0xUohpT4`
+
+Relevant Figma nodes previously inspected/used:
+
+- Mobile Player node: `21:4432`
+- Play button states: `40:99`
+
+The user exported button PNGs at `@1x`, `@2x`, and `@3x`. Figma default `@1x` export has no suffix; these were mapped into iOS imagesets and Android drawable resources.
+
+## Important Decisions Already Made
+
+- The app should use native projects for iOS and Android.
+- React component generation is not needed.
+- iOS should use the custom player, not `AVPlayerViewController`, for the product direction.
+- Main page should not embed a player placeholder.
+- Clicking play/add should open a full-screen player.
+- Player should not show subtitle.
+- iOS control button blur uses `.systemUltraThinMaterialDark`.
+- Android uses a 25% black circular background behind transparent button icons for now.
+- Side controls should show only one set:
+  - 15-second seek controls, or
+  - previous/next controls, or
+  - no side controls.
+
+## Known Gaps / Next Steps
+
+- Android build needs Gradle wrapper or Android Studio sync.
+- Android playback should likely be upgraded from `VideoView` to Media3 ExoPlayer for robust `.m3u8` and `.mp4` parity.
+- Android progress info is currently static in the overlay and should be wired to playback position/duration.
+- Android drag-to-seek is not yet implemented.
+- iOS AirPlay, playback speed, mute, and full-screen controls were discussed earlier, but the current restored custom player only has the implemented subset described above.
+- History is in-memory only on both platforms; persistence has not been added.
+- No automated tests are currently present.
+
+## Suggested Next Implementation Order
+
+1. Verify Android project in Android Studio or add `gradlew`.
+2. Replace Android `VideoView` with Media3 ExoPlayer.
+3. Wire Android progress, duration, and drag-to-seek.
+4. Add persistent history storage on both platforms.
+5. Add player speed, mute, full-screen/orientation behavior, and AirPlay where required.
+6. Add focused unit/UI tests for URL validation and player state transitions.

+ 42 - 0
README.md

@@ -0,0 +1,42 @@
+# GMPlayer
+
+GMPlayer is a native mobile app workspace for iOS and Android.
+
+## Project Layout
+
+- `ios/GMPlayer.xcodeproj` - iOS app project written in Swift.
+- `ios/GMPlayer` - iOS source code.
+- `android` - Android app project written in Kotlin with Jetpack Compose.
+- `docs/requirements.md` - shared product requirements and platform task mapping.
+
+## iOS
+
+Open the project in Xcode:
+
+```sh
+open ios/GMPlayer.xcodeproj
+```
+
+Build from the command line:
+
+```sh
+xcodebuild -project ios/GMPlayer.xcodeproj -scheme GMPlayer -destination 'platform=iOS Simulator,name=iPhone 16' build
+```
+
+## Android
+
+The Android project expects Android Studio or a local Gradle/JDK installation.
+
+```sh
+cd android
+./gradlew :app:assembleDebug
+```
+
+If `gradlew` is not present yet, open the `android` folder in Android Studio and let it create or sync the Gradle wrapper.
+
+## Development Workflow
+
+1. Put shared behavior, UX, and acceptance criteria in `docs/requirements.md`.
+2. Implement platform-specific UI and system integration natively.
+3. Keep feature names, state names, and acceptance criteria aligned across both apps.
+4. Verify both platforms before marking a feature done.

+ 31 - 0
android/app/build.gradle.kts

@@ -0,0 +1,31 @@
+plugins {
+    alias(libs.plugins.android.application)
+    alias(libs.plugins.kotlin.android)
+    alias(libs.plugins.kotlin.compose)
+}
+
+android {
+    namespace = "com.gmplayer.app"
+    compileSdk = 36
+
+    defaultConfig {
+        applicationId = "com.gmplayer.app"
+        minSdk = 26
+        targetSdk = 36
+        versionCode = 1
+        versionName = "0.1.0"
+    }
+
+    buildFeatures {
+        compose = true
+    }
+}
+
+dependencies {
+    implementation(libs.androidx.core.ktx)
+    implementation(libs.androidx.activity.compose)
+    implementation(platform(libs.androidx.compose.bom))
+    implementation(libs.androidx.compose.ui)
+    implementation(libs.androidx.compose.ui.tooling.preview)
+    implementation(libs.androidx.compose.material3)
+}

+ 22 - 0
android/app/src/main/AndroidManifest.xml

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+    <uses-permission android:name="android.permission.INTERNET" />
+
+    <application
+        android:allowBackup="true"
+        android:usesCleartextTraffic="true"
+        android:icon="@mipmap/ic_launcher"
+        android:label="GMPlayer"
+        android:roundIcon="@mipmap/ic_launcher_round"
+        android:supportsRtl="true"
+        android:theme="@style/AppTheme">
+        <activity
+            android:name=".MainActivity"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>

+ 342 - 0
android/app/src/main/java/com/gmplayer/app/MainActivity.kt

@@ -0,0 +1,342 @@
+package com.gmplayer.app
+
+import android.net.Uri
+import android.os.Bundle
+import android.widget.VideoView
+import androidx.activity.compose.BackHandler
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableLongStateOf
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardCapitalization
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import kotlinx.coroutines.delay
+
+class MainActivity : ComponentActivity() {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContent {
+            GMPlayerApp()
+        }
+    }
+}
+
+@Composable
+private fun GMPlayerApp() {
+    MaterialTheme(
+        colorScheme = lightColorScheme(
+            primary = Color(0xFF266C72),
+            background = Color(0xFFF4F7F8),
+            surface = Color.White
+        )
+    ) {
+        Surface(
+            modifier = Modifier.fillMaxSize(),
+            color = MaterialTheme.colorScheme.background
+        ) {
+            PlayerScreen()
+        }
+    }
+}
+
+@Composable
+private fun PlayerScreen() {
+    var inputValue by remember { mutableStateOf("") }
+    var message by remember { mutableStateOf("") }
+    var isError by remember { mutableStateOf(false) }
+    var playingUrl by remember { mutableStateOf<String?>(null) }
+    val recentUrls = remember { mutableStateListOf<String>() }
+
+    fun addUrl(rawValue: String) {
+        val url = rawValue.trim()
+        if (!isSupportedVideoUrl(url)) {
+            isError = true
+            message = "Enter a valid http or https video URL ending in .m3u8 or .mp4."
+            return
+        }
+
+        isError = false
+        message = "Added ${Uri.parse(url).lastPathSegment ?: url}"
+        recentUrls.remove(url)
+        recentUrls.add(0, url)
+        while (recentUrls.size > 3) {
+            recentUrls.removeAt(recentUrls.lastIndex)
+        }
+        playingUrl = url
+    }
+
+    playingUrl?.let { url ->
+        AndroidPlayerScreen(
+            url = url,
+            onClose = { playingUrl = null }
+        )
+        return
+    }
+
+    Column(
+        modifier = Modifier
+            .fillMaxSize()
+            .padding(horizontal = 20.dp, vertical = 28.dp),
+        verticalArrangement = Arrangement.spacedBy(18.dp)
+    ) {
+        Text(
+            text = "Video URLs",
+            style = MaterialTheme.typography.headlineMedium,
+            fontWeight = FontWeight.Bold
+        )
+        Text(
+            text = "Enter an mp4 or m3u8 video URL to keep it in the list.",
+            style = MaterialTheme.typography.bodyMedium,
+            color = Color(0xFF5E6A70)
+        )
+        Row(
+            modifier = Modifier.fillMaxWidth(),
+            horizontalArrangement = Arrangement.spacedBy(10.dp)
+        ) {
+            OutlinedTextField(
+                value = inputValue,
+                onValueChange = { inputValue = it },
+                modifier = Modifier.weight(1f),
+                singleLine = true,
+                placeholder = { Text("https://example.com/video.m3u8") },
+                keyboardOptions = KeyboardOptions(
+                    capitalization = KeyboardCapitalization.None,
+                    keyboardType = KeyboardType.Uri,
+                    imeAction = ImeAction.Go
+                ),
+                keyboardActions = KeyboardActions(onGo = { addUrl(inputValue) })
+            )
+            Button(
+                onClick = { addUrl(inputValue) },
+                colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary),
+                shape = RoundedCornerShape(8.dp)
+            ) {
+                Text("Add", fontWeight = FontWeight.SemiBold)
+            }
+        }
+        if (message.isNotBlank()) {
+            Text(
+                text = message,
+                style = MaterialTheme.typography.bodySmall,
+                color = if (isError) Color(0xFFC62828) else Color(0xFF5E6A70)
+            )
+        }
+        Text(
+            text = "History",
+            style = MaterialTheme.typography.titleMedium,
+            fontWeight = FontWeight.SemiBold
+        )
+        if (recentUrls.isEmpty()) {
+            QueueItem("No videos added yet", enabled = false, onClick = {})
+        } else {
+            recentUrls.forEach { url ->
+                QueueItem(url, enabled = true) {
+                    inputValue = url
+                }
+            }
+        }
+    }
+}
+
+@Composable
+private fun AndroidPlayerScreen(url: String, onClose: () -> Unit) {
+    var controlsVisible by remember { mutableStateOf(true) }
+    var autoHideTick by remember { mutableLongStateOf(0L) }
+    var controllerState by remember { mutableStateOf(MobileMainControllerState.Play) }
+    var videoView by remember { mutableStateOf<VideoView?>(null) }
+
+    fun registerControlsInteraction() {
+        controlsVisible = true
+        autoHideTick += 1
+    }
+
+    BackHandler(onBack = onClose)
+
+    LaunchedEffect(controlsVisible, autoHideTick) {
+        if (controlsVisible) {
+            delay(4_000)
+            controlsVisible = false
+        }
+    }
+
+    DisposableEffect(Unit) {
+        onDispose {
+            videoView?.stopPlayback()
+        }
+    }
+
+    Box(
+        modifier = Modifier
+            .fillMaxSize()
+            .clickable {
+                controlsVisible = !controlsVisible
+                if (controlsVisible) {
+                    autoHideTick += 1
+                }
+            },
+        contentAlignment = Alignment.Center
+    ) {
+        AndroidView(
+            modifier = Modifier.fillMaxSize(),
+            factory = { context ->
+                VideoView(context).apply {
+                    setVideoURI(Uri.parse(url))
+                    setOnPreparedListener { mediaPlayer ->
+                        mediaPlayer.isLooping = false
+                        start()
+                        controllerState = MobileMainControllerState.Pause
+                        registerControlsInteraction()
+                    }
+                    setOnCompletionListener {
+                        controllerState = MobileMainControllerState.Replay
+                        controlsVisible = true
+                    }
+                    videoView = this
+                }
+            },
+            update = { view ->
+                if (videoView !== view) {
+                    videoView = view
+                }
+            }
+        )
+
+        AnimatedVisibility(
+            visible = controlsVisible,
+            enter = fadeIn(animationSpec = tween(durationMillis = 220)),
+            exit = fadeOut(animationSpec = tween(durationMillis = 220)),
+            modifier = Modifier.align(Alignment.Center)
+        ) {
+            MobilePlaybackControls(
+                state = controllerState,
+                onBack15 = {
+                    registerControlsInteraction()
+                    videoView?.let { view ->
+                        view.seekTo((view.currentPosition - 15_000).coerceAtLeast(0))
+                    }
+                },
+                onMainClick = {
+                    registerControlsInteraction()
+                    videoView?.let { view ->
+                        when (controllerState) {
+                            MobileMainControllerState.Replay -> {
+                                view.seekTo(0)
+                                view.start()
+                                controllerState = MobileMainControllerState.Pause
+                            }
+                            MobileMainControllerState.Pause -> {
+                                view.pause()
+                                controllerState = MobileMainControllerState.Play
+                            }
+                            MobileMainControllerState.Play -> {
+                                view.start()
+                                controllerState = MobileMainControllerState.Pause
+                            }
+                        }
+                    }
+                },
+                onForward15 = {
+                    registerControlsInteraction()
+                    videoView?.let { view ->
+                        val duration = view.duration.takeIf { it > 0 } ?: Int.MAX_VALUE
+                        view.seekTo((view.currentPosition + 15_000).coerceAtMost(duration))
+                    }
+                },
+                onPrevious = { registerControlsInteraction() },
+                onNext = { registerControlsInteraction() }
+            )
+        }
+
+        AnimatedVisibility(
+            visible = controlsVisible,
+            enter = fadeIn(animationSpec = tween(durationMillis = 220)),
+            exit = fadeOut(animationSpec = tween(durationMillis = 220)),
+            modifier = Modifier
+                .align(Alignment.BottomCenter)
+                .height(106.dp)
+                .clickable { registerControlsInteraction() }
+        ) {
+            MobileProgressBarInfo(
+                title = Uri.parse(url).lastPathSegment ?: "Internet Video",
+                info = "",
+                currentTime = "--:--",
+                totalTime = "--:--",
+                progress = 0f,
+                showInfo = false
+            )
+        }
+    }
+}
+
+@Composable
+private fun QueueItem(title: String, enabled: Boolean, onClick: () -> Unit) {
+    Surface(
+        onClick = onClick,
+        enabled = enabled,
+        modifier = Modifier.fillMaxWidth(),
+        color = MaterialTheme.colorScheme.surface,
+        shape = RoundedCornerShape(8.dp)
+    ) {
+        Text(
+            text = title,
+            modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp),
+            style = MaterialTheme.typography.bodyLarge,
+            color = if (enabled) Color.Black else Color(0xFF8A8F94),
+            maxLines = 1,
+            overflow = TextOverflow.Ellipsis
+        )
+    }
+}
+
+private fun isSupportedVideoUrl(value: String): Boolean {
+    val uri = Uri.parse(value)
+    val scheme = uri.scheme?.lowercase()
+    if (scheme != "http" && scheme != "https") return false
+    val path = uri.path?.lowercase().orEmpty()
+    return path.endsWith(".m3u8") || path.endsWith(".mp4")
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun PlayerScreenPreview() {
+    GMPlayerApp()
+}

+ 60 - 0
android/app/src/main/java/com/gmplayer/app/MobileMainControllerButton.kt

@@ -0,0 +1,60 @@
+package com.gmplayer.app
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+
+enum class MobileMainControllerState {
+    Pause,
+    Play,
+    Replay
+}
+
+@Composable
+fun MobileMainControllerButton(
+    state: MobileMainControllerState,
+    modifier: Modifier = Modifier,
+    onClick: () -> Unit = {}
+) {
+    val imageResource = when (state) {
+        MobileMainControllerState.Pause -> R.drawable.player_button_pause
+        MobileMainControllerState.Play -> R.drawable.player_button_play
+        MobileMainControllerState.Replay -> R.drawable.player_button_replay
+    }
+    val description = when (state) {
+        MobileMainControllerState.Pause -> "Pause"
+        MobileMainControllerState.Play -> "Play"
+        MobileMainControllerState.Replay -> "Replay"
+    }
+
+    Box(
+        modifier = modifier
+            .size(98.dp)
+            .clip(CircleShape)
+            .background(Color.Black.copy(alpha = 0.25f))
+            .clickable(role = Role.Button, onClick = onClick)
+    ) {
+        Image(
+            painter = painterResource(id = imageResource),
+            contentDescription = description,
+            modifier = Modifier.size(98.dp)
+        )
+    }
+}
+
+@Preview(showBackground = true, backgroundColor = 0xFF20242C)
+@Composable
+private fun MobileMainControllerButtonPreview() {
+    MobileMainControllerButton(state = MobileMainControllerState.Play)
+}

+ 131 - 0
android/app/src/main/java/com/gmplayer/app/MobilePlaybackControls.kt

@@ -0,0 +1,131 @@
+package com.gmplayer.app
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+
+enum class MobilePlaybackSideControlsMode {
+    Skip15,
+    Playlist,
+    Hidden
+}
+
+@Composable
+fun MobileSkip15Button(
+    forward: Boolean,
+    modifier: Modifier = Modifier,
+    onClick: () -> Unit = {}
+) {
+    Box(
+        modifier = modifier
+            .size(66.dp)
+            .clip(CircleShape)
+            .background(Color.Black.copy(alpha = 0.25f))
+            .clickable(role = Role.Button, onClick = onClick)
+    ) {
+        Image(
+            painter = painterResource(
+                id = if (forward) {
+                    R.drawable.player_button_go_forward_15
+                } else {
+                    R.drawable.player_button_go_backwards_15
+                }
+            ),
+            contentDescription = if (forward) "Forward 15 seconds" else "Back 15 seconds",
+            modifier = Modifier.size(66.dp)
+        )
+    }
+}
+
+@Composable
+fun MobileSkipVideoButton(
+    forward: Boolean,
+    modifier: Modifier = Modifier,
+    onClick: () -> Unit = {}
+) {
+    Box(
+        modifier = modifier
+            .size(66.dp)
+            .clip(CircleShape)
+            .background(Color.Black.copy(alpha = 0.25f))
+            .clickable(role = Role.Button, onClick = onClick)
+    ) {
+        Image(
+            painter = painterResource(
+                id = if (forward) {
+                    R.drawable.player_button_skip_forward
+                } else {
+                    R.drawable.player_button_skip_backward
+                }
+            ),
+            contentDescription = if (forward) "Next video" else "Previous video",
+            modifier = Modifier.size(66.dp)
+        )
+    }
+}
+
+@Composable
+fun MobilePlaybackControls(
+    state: MobileMainControllerState,
+    modifier: Modifier = Modifier,
+    sideControlsMode: MobilePlaybackSideControlsMode = PlayerControlsConfiguration.sideControlsMode,
+    onPrevious: () -> Unit = {},
+    onBack15: () -> Unit = {},
+    onMainClick: () -> Unit = {},
+    onForward15: () -> Unit = {},
+    onNext: () -> Unit = {}
+) {
+    Row(
+        modifier = modifier,
+        horizontalArrangement = Arrangement.Start,
+        verticalAlignment = Alignment.CenterVertically
+    ) {
+        when (sideControlsMode) {
+            MobilePlaybackSideControlsMode.Skip15 -> {
+                MobileSkip15Button(forward = false, onClick = onBack15)
+                Spacer(modifier = Modifier.width(16.dp))
+            }
+            MobilePlaybackSideControlsMode.Playlist -> {
+                MobileSkipVideoButton(forward = false, onClick = onPrevious)
+                Spacer(modifier = Modifier.width(16.dp))
+            }
+            MobilePlaybackSideControlsMode.Hidden -> Unit
+        }
+
+        MobileMainControllerButton(state = state, onClick = onMainClick)
+
+        when (sideControlsMode) {
+            MobilePlaybackSideControlsMode.Skip15 -> {
+                Spacer(modifier = Modifier.width(16.dp))
+                MobileSkip15Button(forward = true, onClick = onForward15)
+            }
+            MobilePlaybackSideControlsMode.Playlist -> {
+                Spacer(modifier = Modifier.width(16.dp))
+                MobileSkipVideoButton(forward = true, onClick = onNext)
+            }
+            MobilePlaybackSideControlsMode.Hidden -> Unit
+        }
+    }
+}
+
+@Preview(showBackground = true, backgroundColor = 0xFF20242C)
+@Composable
+private fun MobilePlaybackControlsPreview() {
+    MobilePlaybackControls(state = MobileMainControllerState.Play)
+}

+ 125 - 0
android/app/src/main/java/com/gmplayer/app/MobileProgressBarInfo.kt

@@ -0,0 +1,125 @@
+package com.gmplayer.app
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.foundation.shape.RoundedCornerShape
+
+@Composable
+fun MobileProgressBarInfo(
+    modifier: Modifier = Modifier,
+    title: String = "Title here",
+    info: String = "Info Text here",
+    currentTime: String = "47:38",
+    totalTime: String = "1:52:32",
+    progress: Float = 0.3846f,
+    showTitle: Boolean = true,
+    showInfo: Boolean = true,
+    showTime: Boolean = true,
+    showProgressBar: Boolean = true
+) {
+    val clampedProgress = progress.coerceIn(0f, 1f)
+
+    Column(
+        modifier = modifier
+            .fillMaxWidth()
+            .background(
+                Brush.verticalGradient(
+                    colorStops = arrayOf(
+                        0f to Color(0x000D0F14),
+                        0.8037f to Color(0xE60D0F14)
+                    )
+                )
+            )
+            .padding(start = 32.dp, top = 48.dp, end = 32.dp, bottom = 24.dp),
+        verticalArrangement = Arrangement.spacedBy(12.dp)
+    ) {
+        Row(
+            modifier = Modifier.fillMaxWidth(),
+            horizontalArrangement = Arrangement.spacedBy(24.dp),
+            verticalAlignment = Alignment.Bottom
+        ) {
+            Column(
+                modifier = Modifier.weight(1f),
+                verticalArrangement = Arrangement.spacedBy(4.dp)
+            ) {
+                if (showTitle) {
+                    Text(
+                        text = title,
+                        color = Color.White,
+                        fontSize = 20.sp,
+                        fontWeight = FontWeight.Normal,
+                        lineHeight = 20.sp,
+                        maxLines = 1,
+                        overflow = TextOverflow.Ellipsis
+                    )
+                }
+                if (showInfo) {
+                    Text(
+                        text = info,
+                        color = Color(0xFFBBBBBB),
+                        fontSize = 14.sp,
+                        fontWeight = FontWeight.Light,
+                        maxLines = 1,
+                        overflow = TextOverflow.Ellipsis
+                    )
+                }
+            }
+
+            if (showTime) {
+                Text(
+                    text = "$currentTime / $totalTime",
+                    color = Color.White,
+                    fontSize = 14.sp,
+                    fontWeight = FontWeight.Light,
+                    lineHeight = 14.sp,
+                    maxLines = 1
+                )
+            }
+        }
+
+        if (showProgressBar) {
+            Box(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .height(2.dp)
+                    .clip(RoundedCornerShape(20.dp))
+                    .background(Color(0x66D9D9D9))
+            ) {
+                Box(
+                    modifier = Modifier
+                        .fillMaxWidth(clampedProgress)
+                        .height(2.dp)
+                        .clip(RoundedCornerShape(20.dp))
+                        .background(Color.White)
+                )
+            }
+        }
+    }
+}
+
+@Preview(showBackground = true, backgroundColor = 0xFF20242C)
+@Composable
+private fun MobileProgressBarInfoPreview() {
+    MaterialTheme {
+        MobileProgressBarInfo()
+    }
+}

+ 5 - 0
android/app/src/main/java/com/gmplayer/app/PlayerControlsConfiguration.kt

@@ -0,0 +1,5 @@
+package com.gmplayer.app
+
+object PlayerControlsConfiguration {
+    val sideControlsMode: MobilePlaybackSideControlsMode = MobilePlaybackSideControlsMode.Skip15
+}

BIN
android/app/src/main/res/drawable-mdpi/player_button_go_backwards_15.png


BIN
android/app/src/main/res/drawable-mdpi/player_button_go_forward_15.png


BIN
android/app/src/main/res/drawable-mdpi/player_button_pause.png


BIN
android/app/src/main/res/drawable-mdpi/player_button_play.png


BIN
android/app/src/main/res/drawable-mdpi/player_button_replay.png


BIN
android/app/src/main/res/drawable-mdpi/player_button_skip_backward.png


BIN
android/app/src/main/res/drawable-mdpi/player_button_skip_forward.png


BIN
android/app/src/main/res/drawable-nodpi/player_button_go_backwards_15.png


BIN
android/app/src/main/res/drawable-nodpi/player_button_go_forward_15.png


BIN
android/app/src/main/res/drawable-nodpi/player_button_pause.png


BIN
android/app/src/main/res/drawable-nodpi/player_button_play.png


BIN
android/app/src/main/res/drawable-nodpi/player_button_replay.png


BIN
android/app/src/main/res/drawable-nodpi/player_button_skip_backward.png


BIN
android/app/src/main/res/drawable-nodpi/player_button_skip_forward.png


BIN
android/app/src/main/res/drawable-xhdpi/player_button_go_backwards_15.png


BIN
android/app/src/main/res/drawable-xhdpi/player_button_go_forward_15.png


BIN
android/app/src/main/res/drawable-xhdpi/player_button_pause.png


BIN
android/app/src/main/res/drawable-xhdpi/player_button_play.png


BIN
android/app/src/main/res/drawable-xhdpi/player_button_replay.png


BIN
android/app/src/main/res/drawable-xhdpi/player_button_skip_backward.png


BIN
android/app/src/main/res/drawable-xhdpi/player_button_skip_forward.png


BIN
android/app/src/main/res/drawable-xxxhdpi/player_button_go_backwards_15.png


BIN
android/app/src/main/res/drawable-xxxhdpi/player_button_go_forward_15.png


BIN
android/app/src/main/res/drawable-xxxhdpi/player_button_pause.png


BIN
android/app/src/main/res/drawable-xxxhdpi/player_button_play.png


BIN
android/app/src/main/res/drawable-xxxhdpi/player_button_replay.png


BIN
android/app/src/main/res/drawable-xxxhdpi/player_button_skip_backward.png


BIN
android/app/src/main/res/drawable-xxxhdpi/player_button_skip_forward.png


+ 13 - 0
android/app/src/main/res/drawable/ic_launcher.xml

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="108"
+    android:viewportHeight="108">
+    <path
+        android:fillColor="#266C72"
+        android:pathData="M0,0h108v108h-108z" />
+    <path
+        android:fillColor="#FFFFFF"
+        android:pathData="M42,31l34,23l-34,23z" />
+</vector>

BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png


BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png


BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png


BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png


BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png


BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png


BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png


BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png


BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png


BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png


+ 8 - 0
android/app/src/main/res/values/styles.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <style name="AppTheme" parent="android:style/Theme.Material.Light.NoActionBar">
+        <item name="android:windowLightStatusBar">true</item>
+        <item name="android:statusBarColor">#F4F7F8</item>
+        <item name="android:navigationBarColor">#F4F7F8</item>
+    </style>
+</resources>

+ 5 - 0
android/build.gradle.kts

@@ -0,0 +1,5 @@
+plugins {
+    alias(libs.plugins.android.application) apply false
+    alias(libs.plugins.kotlin.android) apply false
+    alias(libs.plugins.kotlin.compose) apply false
+}

+ 19 - 0
android/gradle/libs.versions.toml

@@ -0,0 +1,19 @@
+[versions]
+agp = "9.2.0"
+kotlin = "2.3.0"
+composeBom = "2026.05.00"
+coreKtx = "1.18.0"
+activityCompose = "1.12.0"
+
+[libraries]
+androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
+androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
+androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }
+androidx-compose-ui = { module = "androidx.compose.ui:ui" }
+androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
+androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }

+ 18 - 0
android/settings.gradle.kts

@@ -0,0 +1,18 @@
+pluginManagement {
+    repositories {
+        google()
+        mavenCentral()
+        gradlePluginPortal()
+    }
+}
+
+dependencyResolutionManagement {
+    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+    repositories {
+        google()
+        mavenCentral()
+    }
+}
+
+rootProject.name = "GMPlayer"
+include(":app")

BIN
design-assets/player-buttons/player_button_go_backwards_15.png


BIN
design-assets/player-buttons/player_button_go_backwards_15@2x.png


BIN
design-assets/player-buttons/player_button_go_backwards_15@3x.png


BIN
design-assets/player-buttons/player_button_go_forward_15.png


BIN
design-assets/player-buttons/player_button_go_forward_15@2x.png


BIN
design-assets/player-buttons/player_button_go_forward_15@3x.png


BIN
design-assets/player-buttons/player_button_pause.png


BIN
design-assets/player-buttons/player_button_pause@2x.png


BIN
design-assets/player-buttons/player_button_pause@3x.png


BIN
design-assets/player-buttons/player_button_play.png


BIN
design-assets/player-buttons/player_button_play@2x.png


BIN
design-assets/player-buttons/player_button_play@3x.png


BIN
design-assets/player-buttons/player_button_replay.png


BIN
design-assets/player-buttons/player_button_replay@2x.png


BIN
design-assets/player-buttons/player_button_replay@3x.png


BIN
design-assets/player-buttons/player_button_skip_backward.png


BIN
design-assets/player-buttons/player_button_skip_backward@2x.png


BIN
design-assets/player-buttons/player_button_skip_backward@3x.png


BIN
design-assets/player-buttons/player_button_skip_forward.png


BIN
design-assets/player-buttons/player_button_skip_forward@2x.png


BIN
design-assets/player-buttons/player_button_skip_forward@3x.png


BIN
docs/app-icon-master.png


+ 55 - 0
docs/requirements.md

@@ -0,0 +1,55 @@
+# GMPlayer Requirements
+
+## Product Direction
+
+GMPlayer is a mobile app developed natively for iOS and Android. The current scaffold provides a simple video URL entry screen with local history while detailed product requirements are still being defined.
+
+## Platform Choices
+
+- iOS: Swift with UIKit.
+- Android: Kotlin with Jetpack Compose.
+
+## Shared Feature Template
+
+Use this format for every feature so both platforms can be developed in parallel.
+
+### Feature
+
+- Name:
+- User goal:
+- Entry point:
+- Primary states:
+- Empty state:
+- Error state:
+- Offline behavior:
+- Analytics events:
+
+### iOS Tasks
+
+- UI:
+- State:
+- System APIs:
+- Tests:
+
+### Android Tasks
+
+- UI:
+- State:
+- System APIs:
+- Tests:
+
+### Acceptance Criteria
+
+- iOS:
+- Android:
+- Shared:
+
+## Initial Screen
+
+The initial screen should show:
+
+- App title.
+- Video address input.
+- Add button.
+- Validation message area.
+- A compact history list.

+ 271 - 0
ios/GMPlayer.xcodeproj/project.pbxproj

@@ -0,0 +1,271 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 56;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		1A0000010000000000000001 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A0000010000000000000011 /* AppDelegate.swift */; };
+		1A0000020000000000000001 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A0000020000000000000011 /* SceneDelegate.swift */; };
+		1A0000030000000000000001 /* PlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A0000030000000000000011 /* PlayerViewController.swift */; };
+		1A0000070000000000000001 /* MobileProgressBarInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A0000070000000000000011 /* MobileProgressBarInfoView.swift */; };
+		1A0000080000000000000001 /* MobileMainControllerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A0000080000000000000011 /* MobileMainControllerButton.swift */; };
+		1A0000090000000000000001 /* MobileSkipButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A0000090000000000000011 /* MobileSkipButton.swift */; };
+		1A00000A0000000000000001 /* MobilePlaybackControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A00000A0000000000000011 /* MobilePlaybackControlsView.swift */; };
+		1A00000B0000000000000001 /* VideoPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A00000B0000000000000011 /* VideoPlayerViewController.swift */; };
+		1A00000C0000000000000001 /* PlayerControlsConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A00000C0000000000000011 /* PlayerControlsConfiguration.swift */; };
+		1A0000060000000000000001 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1A0000060000000000000011 /* Assets.xcassets */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXFileReference section */
+		1A0000010000000000000011 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
+		1A0000020000000000000011 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
+		1A0000030000000000000011 /* PlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewController.swift; sourceTree = "<group>"; };
+		1A0000040000000000000011 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		1A0000050000000000000011 /* GMPlayer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GMPlayer.app; sourceTree = BUILT_PRODUCTS_DIR; };
+		1A0000060000000000000011 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
+		1A0000070000000000000011 /* MobileProgressBarInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MobileProgressBarInfoView.swift; sourceTree = "<group>"; };
+		1A0000080000000000000011 /* MobileMainControllerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MobileMainControllerButton.swift; sourceTree = "<group>"; };
+		1A0000090000000000000011 /* MobileSkipButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MobileSkipButton.swift; sourceTree = "<group>"; };
+		1A00000A0000000000000011 /* MobilePlaybackControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MobilePlaybackControlsView.swift; sourceTree = "<group>"; };
+		1A00000B0000000000000011 /* VideoPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewController.swift; sourceTree = "<group>"; };
+		1A00000C0000000000000011 /* PlayerControlsConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerControlsConfiguration.swift; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		1A0000010000000000000021 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		1A0000010000000000000031 = {
+			isa = PBXGroup;
+			children = (
+				1A0000020000000000000031 /* GMPlayer */,
+				1A0000030000000000000031 /* Products */,
+			);
+			sourceTree = "<group>";
+		};
+		1A0000020000000000000031 /* GMPlayer */ = {
+			isa = PBXGroup;
+			children = (
+				1A0000010000000000000011 /* AppDelegate.swift */,
+				1A0000020000000000000011 /* SceneDelegate.swift */,
+				1A0000030000000000000011 /* PlayerViewController.swift */,
+				1A0000070000000000000011 /* MobileProgressBarInfoView.swift */,
+				1A0000080000000000000011 /* MobileMainControllerButton.swift */,
+				1A0000090000000000000011 /* MobileSkipButton.swift */,
+				1A00000A0000000000000011 /* MobilePlaybackControlsView.swift */,
+				1A00000B0000000000000011 /* VideoPlayerViewController.swift */,
+				1A00000C0000000000000011 /* PlayerControlsConfiguration.swift */,
+				1A0000040000000000000011 /* Info.plist */,
+				1A0000060000000000000011 /* Assets.xcassets */,
+			);
+			path = GMPlayer;
+			sourceTree = "<group>";
+		};
+		1A0000030000000000000031 /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				1A0000050000000000000011 /* GMPlayer.app */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		1A0000010000000000000041 /* GMPlayer */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 1A0000010000000000000071 /* Build configuration list for PBXNativeTarget "GMPlayer" */;
+			buildPhases = (
+				1A0000010000000000000051 /* Sources */,
+				1A0000010000000000000021 /* Frameworks */,
+				1A0000020000000000000051 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = GMPlayer;
+			productName = GMPlayer;
+			productReference = 1A0000050000000000000011 /* GMPlayer.app */;
+			productType = "com.apple.product-type.application";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		1A0000010000000000000061 /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				BuildIndependentTargetsInParallel = 1;
+				LastSwiftUpdateCheck = 1600;
+				LastUpgradeCheck = 1600;
+				TargetAttributes = {
+					1A0000010000000000000041 = {
+						CreatedOnToolsVersion = 16.0;
+					};
+				};
+			};
+			buildConfigurationList = 1A0000020000000000000071 /* Build configuration list for PBXProject "GMPlayer" */;
+			compatibilityVersion = "Xcode 14.0";
+			developmentRegion = en;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+				Base,
+			);
+			mainGroup = 1A0000010000000000000031;
+			productRefGroup = 1A0000030000000000000031 /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				1A0000010000000000000041 /* GMPlayer */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		1A0000020000000000000051 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				1A0000060000000000000001 /* Assets.xcassets in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		1A0000010000000000000051 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				1A0000010000000000000001 /* AppDelegate.swift in Sources */,
+				1A0000020000000000000001 /* SceneDelegate.swift in Sources */,
+				1A0000030000000000000001 /* PlayerViewController.swift in Sources */,
+				1A0000070000000000000001 /* MobileProgressBarInfoView.swift in Sources */,
+				1A0000080000000000000001 /* MobileMainControllerButton.swift in Sources */,
+				1A0000090000000000000001 /* MobileSkipButton.swift in Sources */,
+				1A00000A0000000000000001 /* MobilePlaybackControlsView.swift in Sources */,
+				1A00000B0000000000000001 /* VideoPlayerViewController.swift in Sources */,
+				1A00000C0000000000000001 /* PlayerControlsConfiguration.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+		1A0000010000000000000081 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				ASSETCATALOG_COMPILER_OPTIMIZATION = space;
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_TEAM = "";
+				GENERATE_INFOPLIST_FILE = NO;
+				INFOPLIST_FILE = GMPlayer/Info.plist;
+				IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+				MARKETING_VERSION = 0.1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = com.gmplayer.app;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Debug;
+		};
+		1A0000020000000000000081 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				ASSETCATALOG_COMPILER_OPTIMIZATION = space;
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_TEAM = "";
+				GENERATE_INFOPLIST_FILE = NO;
+				INFOPLIST_FILE = GMPlayer/Info.plist;
+				IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+				MARKETING_VERSION = 0.1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = com.gmplayer.app;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Release;
+		};
+		1A0000030000000000000081 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = dwarf;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_TESTABILITY = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu17;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = iphoneos;
+				SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+			};
+			name = Debug;
+		};
+		1A0000040000000000000081 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu17;
+				GCC_NO_COMMON_BLOCKS = YES;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				SDKROOT = iphoneos;
+				SWIFT_COMPILATION_MODE = wholemodule;
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		1A0000010000000000000071 /* Build configuration list for PBXNativeTarget "GMPlayer" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				1A0000010000000000000081 /* Debug */,
+				1A0000020000000000000081 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		1A0000020000000000000071 /* Build configuration list for PBXProject "GMPlayer" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				1A0000030000000000000081 /* Debug */,
+				1A0000040000000000000081 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = 1A0000010000000000000061 /* Project object */;
+}

+ 19 - 0
ios/GMPlayer/AppDelegate.swift

@@ -0,0 +1,19 @@
+import UIKit
+
+@main
+final class AppDelegate: UIResponder, UIApplicationDelegate {
+    func application(
+        _ application: UIApplication,
+        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+    ) -> Bool {
+        true
+    }
+
+    func application(
+        _ application: UIApplication,
+        configurationForConnecting connectingSceneSession: UISceneSession,
+        options: UIScene.ConnectionOptions
+    ) -> UISceneConfiguration {
+        UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
+    }
+}

+ 116 - 0
ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Contents.json

@@ -0,0 +1,116 @@
+{
+  "images": [
+    {
+      "idiom": "iphone",
+      "size": "20x20",
+      "scale": "2x",
+      "filename": "Icon-20@2x.png"
+    },
+    {
+      "idiom": "iphone",
+      "size": "20x20",
+      "scale": "3x",
+      "filename": "Icon-20@3x.png"
+    },
+    {
+      "idiom": "iphone",
+      "size": "29x29",
+      "scale": "2x",
+      "filename": "Icon-29@2x.png"
+    },
+    {
+      "idiom": "iphone",
+      "size": "29x29",
+      "scale": "3x",
+      "filename": "Icon-29@3x.png"
+    },
+    {
+      "idiom": "iphone",
+      "size": "40x40",
+      "scale": "2x",
+      "filename": "Icon-40@2x.png"
+    },
+    {
+      "idiom": "iphone",
+      "size": "40x40",
+      "scale": "3x",
+      "filename": "Icon-40@3x.png"
+    },
+    {
+      "idiom": "iphone",
+      "size": "60x60",
+      "scale": "2x",
+      "filename": "Icon-60@2x.png"
+    },
+    {
+      "idiom": "iphone",
+      "size": "60x60",
+      "scale": "3x",
+      "filename": "Icon-60@3x.png"
+    },
+    {
+      "idiom": "ipad",
+      "size": "20x20",
+      "scale": "1x",
+      "filename": "Icon-20~ipad.png"
+    },
+    {
+      "idiom": "ipad",
+      "size": "20x20",
+      "scale": "2x",
+      "filename": "Icon-20@2x~ipad.png"
+    },
+    {
+      "idiom": "ipad",
+      "size": "29x29",
+      "scale": "1x",
+      "filename": "Icon-29~ipad.png"
+    },
+    {
+      "idiom": "ipad",
+      "size": "29x29",
+      "scale": "2x",
+      "filename": "Icon-29@2x~ipad.png"
+    },
+    {
+      "idiom": "ipad",
+      "size": "40x40",
+      "scale": "1x",
+      "filename": "Icon-40~ipad.png"
+    },
+    {
+      "idiom": "ipad",
+      "size": "40x40",
+      "scale": "2x",
+      "filename": "Icon-40@2x~ipad.png"
+    },
+    {
+      "idiom": "ipad",
+      "size": "76x76",
+      "scale": "1x",
+      "filename": "Icon-76.png"
+    },
+    {
+      "idiom": "ipad",
+      "size": "76x76",
+      "scale": "2x",
+      "filename": "Icon-76@2x.png"
+    },
+    {
+      "idiom": "ipad",
+      "size": "83.5x83.5",
+      "scale": "2x",
+      "filename": "Icon-83.5@2x.png"
+    },
+    {
+      "idiom": "ios-marketing",
+      "size": "1024x1024",
+      "scale": "1x",
+      "filename": "Icon-1024.png"
+    }
+  ],
+  "info": {
+    "author": "xcode",
+    "version": 1
+  }
+}

BIN
ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-1024.png


BIN
ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-20@2x.png


BIN
ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-20@2x~ipad.png


BIN
ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-20@3x.png


BIN
ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-20~ipad.png


BIN
ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-29@2x.png


BIN
ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-29@2x~ipad.png


BIN
ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-29@3x.png


BIN
ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-29~ipad.png


BIN
ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png


BIN
ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-40@2x~ipad.png


BIN
ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png


BIN
ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-40~ipad.png


BIN
ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png


BIN
ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png


BIN
ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-76.png


BIN
ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png


BIN
ios/GMPlayer/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png


+ 23 - 0
ios/GMPlayer/Assets.xcassets/player_button_go_backwards_15.imageset/Contents.json

@@ -0,0 +1,23 @@
+{
+  "images": [
+    {
+      "idiom": "universal",
+      "filename": "player_button_go_backwards_15@1x.png",
+      "scale": "1x"
+    },
+    {
+      "idiom": "universal",
+      "filename": "player_button_go_backwards_15@2x.png",
+      "scale": "2x"
+    },
+    {
+      "idiom": "universal",
+      "filename": "player_button_go_backwards_15@3x.png",
+      "scale": "3x"
+    }
+  ],
+  "info": {
+    "author": "xcode",
+    "version": 1
+  }
+}

BIN
ios/GMPlayer/Assets.xcassets/player_button_go_backwards_15.imageset/player_button_go_backwards_15@1x.png


BIN
ios/GMPlayer/Assets.xcassets/player_button_go_backwards_15.imageset/player_button_go_backwards_15@2x.png


Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov