Initial commit

This commit is contained in:
2025-07-13 14:01:29 +03:00
commit 0eaf91561a
188 changed files with 11616 additions and 0 deletions

43
.gitignore vendored Normal file
View File

@@ -0,0 +1,43 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
.env
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here

45
.metadata Normal file
View File

@@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "fcf2c11572af6f390246c056bc905eca609533a0"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: fcf2c11572af6f390246c056bc905eca609533a0
base_revision: fcf2c11572af6f390246c056bc905eca609533a0
- platform: android
create_revision: fcf2c11572af6f390246c056bc905eca609533a0
base_revision: fcf2c11572af6f390246c056bc905eca609533a0
- platform: ios
create_revision: fcf2c11572af6f390246c056bc905eca609533a0
base_revision: fcf2c11572af6f390246c056bc905eca609533a0
- platform: linux
create_revision: fcf2c11572af6f390246c056bc905eca609533a0
base_revision: fcf2c11572af6f390246c056bc905eca609533a0
- platform: macos
create_revision: fcf2c11572af6f390246c056bc905eca609533a0
base_revision: fcf2c11572af6f390246c056bc905eca609533a0
- platform: web
create_revision: fcf2c11572af6f390246c056bc905eca609533a0
base_revision: fcf2c11572af6f390246c056bc905eca609533a0
- platform: windows
create_revision: fcf2c11572af6f390246c056bc905eca609533a0
base_revision: fcf2c11572af6f390246c056bc905eca609533a0
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

201
LICENSE Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2025 NeoMovies
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

94
README.md Normal file
View File

@@ -0,0 +1,94 @@
# NeoMovies Mobile 🎬
Мобильное приложение для просмотра фильмов и сериалов, созданное на Flutter.
## Возможности
- 📱 Кроссплатформенное приложение (Android/iOS(пока не реализовано))
- 🎥 Просмотр фильмов и сериалов через WebView
- 🌙 Поддержка динамической темы
- 💾 Локальное кэширование данных
- 🔒 Безопасное хранение данных
- 🚀 Быстрая загрузка контента
- 🎨 Современный Material Design интерфейс
## Технологии
- **Flutter** - основной фреймворк
- **Provider** - управление состоянием
- **Hive** - локальная база данных
- **HTTP** - сетевые запросы
- **WebView** - воспроизведение видео
- **Cached Network Image** - кэширование изображений
- **Google Fonts** - красивые шрифты
## Установка
1. Клонируйте репозиторий:
```bash
git clone https://gitlab.com/foxixus/neomovies_mobile.git
cd neomovies_mobile
```
2. Установите зависимости:
```bash
flutter pub get
```
3. Создайте файл `.env` в корне проекта:
```
API_URL=your_api_url_here
```
4. Запустите приложение:
```bash
flutter run
```
## Сборка
### Android APK
```bash
flutter build apk --release
```
### iOS
```bash
flutter build ios --release
```
## Структура проекта
```
lib/
├── main.dart # Точка входа
├── models/ # Модели данных
├── services/ # API сервисы
├── providers/ # State management
├── screens/ # Экраны приложения
├── widgets/ # Переиспользуемые виджеты
└── utils/ # Утилиты и константы
```
## Системные требования
- **Flutter SDK**: 3.8.1+
- **Dart**: 3.8.1+
- **Android**: API 21+ (Android 5.0+)
- **iOS**: iOS 11.0+
## Участие в разработке
1. Форкните репозиторий
2. Создайте ветку для новой функции (`git checkout -b feature/amazing-feature`)
3. Внесите изменения и закоммитьте (`git commit -m 'Add amazing feature'`)
4. Отправьте изменения в ветку (`git push origin feature/amazing-feature`)
5. Создайте Pull Request
## Лицензия
Этот проект лицензирован под Apache 2.0 License - подробности в файле [LICENSE](LICENSE).
## Контакты
Если у вас есть вопросы или предложения, создайте issue в этом репозитории.

28
analysis_options.yaml Normal file
View File

@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

14
android/.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View File

@@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.example.neomovies_mobile"
compileSdk = flutter.compileSdkVersion
ndkVersion = "27.0.12077973"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.neomovies_mobile"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,54 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<!-- Queries for url_launcher -->
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
</queries>
<application
android:label="NeoMovies"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@@ -0,0 +1,5 @@
package com.example.neomovies_mobile
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#00000000</color>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">NeoMovies</string>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

21
android/build.gradle.kts Normal file
View File

@@ -0,0 +1,21 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip

View File

@@ -0,0 +1,25 @@
pluginManagement {
val flutterSdkPath = run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.7.3" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
}
include(":app")

BIN
assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 KiB

4
assets/logo.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
<rect width="1024" height="1024" fill="none"/>
<path d="M256 192h128l256 512V192h128v640H640L384 320v512H256V192z" fill="#FF4B5C"/>
</svg>

After

Width:  |  Height:  |  Size: 235 B

6
assets/monochrome.svg Normal file
View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
<!-- Transparent background -->
<rect width="1024" height="1024" fill="none"/>
<!-- White “N” path for monochrome adaptive icon -->
<path d="M256 192h128l256 512V192h128v640H640L384 320v512H256V192z" fill="#FFFFFF"/>
</svg>

After

Width:  |  Height:  |  Size: 328 B

34
ios/.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View File

@@ -0,0 +1,26 @@
<?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>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
</dict>
</plist>

View File

@@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@@ -0,0 +1,616 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.neomoviesMobile;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.neomoviesMobile.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.neomoviesMobile.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.neomoviesMobile.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.neomoviesMobile;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.neomoviesMobile;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?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>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?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>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?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>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?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>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,13 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

49
ios/Runner/Info.plist Normal file
View File

@@ -0,0 +1,49 @@
<?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>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Neomovies Mobile</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>neomovies_mobile</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

View File

@@ -0,0 +1,332 @@
import 'dart:convert';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:http/http.dart' as http;
import 'package:neomovies_mobile/data/models/auth_response.dart';
import 'package:neomovies_mobile/data/models/favorite.dart';
import 'package:neomovies_mobile/data/models/movie.dart';
import 'package:neomovies_mobile/data/models/reaction.dart';
import 'package:neomovies_mobile/data/models/user.dart';
class ApiClient {
final http.Client _client;
final String _baseUrl = dotenv.env['API_URL']!;
ApiClient(this._client);
Future<List<Movie>> getPopularMovies({int page = 1}) async {
return _fetchMovies('/movies/popular', page: page);
}
Future<List<Movie>> getTopRatedMovies({int page = 1}) async {
return _fetchMovies('/movies/top-rated', page: page);
}
Future<List<Movie>> getUpcomingMovies({int page = 1}) async {
return _fetchMovies('/movies/upcoming', page: page);
}
Future<Movie> getMovieById(String id) async {
return _fetchMovieDetail('/movies/$id');
}
Future<Movie> getTvById(String id) async {
return _fetchMovieDetail('/tv/$id');
}
// Получение IMDB ID для фильмов
Future<String?> getMovieImdbId(int movieId) async {
try {
final uri = Uri.parse('$_baseUrl/movies/$movieId/external-ids');
final response = await _client.get(uri).timeout(const Duration(seconds: 30));
if (response.statusCode == 200) {
final data = json.decode(response.body);
return data['imdb_id'] as String?;
} else {
print('Failed to get movie IMDB ID: ${response.statusCode}');
return null;
}
} catch (e) {
print('Error getting movie IMDB ID: $e');
return null;
}
}
// Получение IMDB ID для сериалов
Future<String?> getTvImdbId(int showId) async {
try {
final uri = Uri.parse('$_baseUrl/tv/$showId/external-ids');
final response = await _client.get(uri).timeout(const Duration(seconds: 30));
if (response.statusCode == 200) {
final data = json.decode(response.body);
return data['imdb_id'] as String?;
} else {
print('Failed to get TV IMDB ID: ${response.statusCode}');
return null;
}
} catch (e) {
print('Error getting TV IMDB ID: $e');
return null;
}
}
// Универсальный метод получения IMDB ID
Future<String?> getImdbId(int mediaId, String mediaType) async {
if (mediaType == 'tv') {
return getTvImdbId(mediaId);
} else {
return getMovieImdbId(mediaId);
}
}
Future<List<Movie>> searchMovies(String query, {int page = 1}) async {
final moviesUri = Uri.parse('$_baseUrl/movies/search?query=${Uri.encodeQueryComponent(query)}&page=$page');
final tvUri = Uri.parse('$_baseUrl/tv/search?query=${Uri.encodeQueryComponent(query)}&page=$page');
final responses = await Future.wait([
_client.get(moviesUri),
_client.get(tvUri),
]);
List<Movie> combined = [];
for (final response in responses) {
if (response.statusCode == 200) {
final decoded = json.decode(response.body);
List<dynamic> listData;
if (decoded is List) {
listData = decoded;
} else if (decoded is Map && decoded['results'] is List) {
listData = decoded['results'];
} else {
listData = [];
}
combined.addAll(listData.map((json) => Movie.fromJson(json)));
} else {
// ignore non-200 but log maybe
}
}
if (combined.isEmpty) {
throw Exception('Failed to search movies/tv');
}
return combined;
}
Future<Movie> _fetchMovieDetail(String path) async {
final uri = Uri.parse('$_baseUrl$path');
final response = await _client.get(uri);
if (response.statusCode == 200) {
final data = json.decode(response.body);
return Movie.fromJson(data);
} else {
throw Exception('Failed to load media details: ${response.statusCode}');
}
}
// Favorites
Future<List<Favorite>> getFavorites() async {
final response = await _client.get(Uri.parse('$_baseUrl/favorites'));
if (response.statusCode == 200) {
final List<dynamic> data = json.decode(response.body);
return data.map((json) => Favorite.fromJson(json)).toList();
} else {
throw Exception('Failed to fetch favorites');
}
}
Future<void> addFavorite(String mediaId, String mediaType, String title, String posterPath) async {
final response = await _client.post(
Uri.parse('$_baseUrl/favorites/$mediaId?mediaType=$mediaType'),
body: json.encode({
'title': title,
'posterPath': posterPath,
}),
);
if (response.statusCode != 201 && response.statusCode != 200) {
throw Exception('Failed to add favorite');
}
}
Future<void> removeFavorite(String mediaId) async {
final response = await _client.delete(
Uri.parse('$_baseUrl/favorites/$mediaId'),
);
if (response.statusCode != 200) {
throw Exception('Failed to remove favorite');
}
}
// Reactions
Future<Map<String, int>> getReactionCounts(String mediaType, String mediaId) async {
final response = await _client.get(
Uri.parse('$_baseUrl/reactions/$mediaType/$mediaId/counts'),
);
print('REACTION COUNTS RESPONSE (${response.statusCode}): ${response.body}');
if (response.statusCode == 200) {
final decoded = json.decode(response.body);
print('PARSED: $decoded');
if (decoded is Map) {
final mapSrc = decoded.containsKey('data') && decoded['data'] is Map
? decoded['data'] as Map<String, dynamic>
: decoded;
print('MAPPING: $mapSrc');
return mapSrc.map((k, v) {
int count;
if (v is num) {
count = v.toInt();
} else if (v is String) {
count = int.tryParse(v) ?? 0;
} else {
count = 0;
}
return MapEntry(k, count);
});
}
if (decoded is List) {
// list of {type,count}
Map<String, int> res = {};
for (var item in decoded) {
if (item is Map && item['type'] != null) {
res[item['type'].toString()] = (item['count'] as num?)?.toInt() ?? 0;
}
}
return res;
}
return {};
} else {
throw Exception('Failed to fetch reactions counts');
}
}
Future<UserReaction> getMyReaction(String mediaType, String mediaId) async {
final response = await _client.get(
Uri.parse('$_baseUrl/reactions/$mediaType/$mediaId/my-reaction'),
);
if (response.statusCode == 200) {
final decoded = json.decode(response.body);
if (decoded == null || (decoded is String && decoded.isEmpty)) {
return UserReaction(reactionType: null);
}
return UserReaction.fromJson(decoded as Map<String, dynamic>);
} else if (response.statusCode == 404) {
return UserReaction(reactionType: 'none'); // No reaction found
} else {
throw Exception('Failed to fetch user reaction');
}
}
Future<void> setReaction(String mediaType, String mediaId, String reactionType) async {
final response = await _client.post(
Uri.parse('$_baseUrl/reactions'),
headers: {'Content-Type': 'application/json'},
body: json.encode({'mediaId': '${mediaType}_${mediaId}', 'type': reactionType}),
);
if (response.statusCode != 201 && response.statusCode != 200 && response.statusCode != 204) {
throw Exception('Failed to set reaction: ${response.statusCode} ${response.body}');
}
}
// --- Auth Methods ---
Future<void> register(String name, String email, String password) async {
final uri = Uri.parse('$_baseUrl/auth/register');
final response = await _client.post(
uri,
headers: {'Content-Type': 'application/json'},
body: json.encode({'name': name, 'email': email, 'password': password}),
);
if (response.statusCode == 201 || response.statusCode == 200) {
final decoded = json.decode(response.body) as Map<String, dynamic>;
if (decoded['success'] == true || decoded.containsKey('token')) {
// registration succeeded; nothing further to return
return;
} else {
throw Exception('Failed to register: ${decoded['message'] ?? 'Unknown error'}');
}
} else {
throw Exception('Failed to register: ${response.statusCode} ${response.body}');
}
}
Future<AuthResponse> login(String email, String password) async {
final uri = Uri.parse('$_baseUrl/auth/login');
final response = await _client.post(
uri,
headers: {'Content-Type': 'application/json'},
body: json.encode({'email': email, 'password': password}),
);
if (response.statusCode == 200) {
return AuthResponse.fromJson(json.decode(response.body));
} else {
throw Exception('Failed to login: ${response.body}');
}
}
Future<void> verify(String email, String code) async {
final uri = Uri.parse('$_baseUrl/auth/verify');
final response = await _client.post(
uri,
headers: {'Content-Type': 'application/json'},
body: json.encode({'email': email, 'code': code}),
);
if (response.statusCode != 200) {
throw Exception('Failed to verify code: ${response.body}');
}
}
Future<void> resendCode(String email) async {
final uri = Uri.parse('$_baseUrl/auth/resend-code');
final response = await _client.post(
uri,
headers: {'Content-Type': 'application/json'},
body: json.encode({'email': email}),
);
if (response.statusCode != 200) {
throw Exception('Failed to resend code: ${response.body}');
}
}
Future<void> deleteAccount() async {
final uri = Uri.parse('$_baseUrl/auth/profile');
final response = await _client.delete(uri);
if (response.statusCode != 200) {
throw Exception('Failed to delete account: ${response.body}');
}
}
// --- Movie Methods ---
Future<List<Movie>> _fetchMovies(String endpoint, {int page = 1}) async {
final uri = Uri.parse('$_baseUrl$endpoint').replace(queryParameters: {
'page': page.toString(),
});
final response = await _client.get(uri);
if (response.statusCode == 200) {
final List<dynamic> data = json.decode(response.body)['results'];
if (data == null) {
return [];
}
return data.map((json) => Movie.fromJson(json)).toList();
} else {
throw Exception('Failed to load movies from $endpoint');
}
}
}

View File

@@ -0,0 +1,19 @@
import 'package:http/http.dart' as http;
import 'package:neomovies_mobile/data/services/secure_storage_service.dart';
class AuthenticatedHttpClient extends http.BaseClient {
final http.Client _inner;
final SecureStorageService _storageService;
AuthenticatedHttpClient(this._storageService, this._inner);
@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
final token = await _storageService.getToken();
if (token != null) {
request.headers['Authorization'] = 'Bearer $token';
}
request.headers['Content-Type'] = 'application/json';
return _inner.send(request);
}
}

View File

@@ -0,0 +1,9 @@
class UnverifiedAccountException implements Exception {
final String email;
final String? message;
UnverifiedAccountException(this.email, {this.message});
@override
String toString() => message ?? 'Account not verified';
}

View File

@@ -0,0 +1,17 @@
import 'package:neomovies_mobile/data/models/user.dart';
class AuthResponse {
final String token;
final User user;
final bool verified;
AuthResponse({required this.token, required this.user, required this.verified});
factory AuthResponse.fromJson(Map<String, dynamic> json) {
return AuthResponse(
token: json['token'] as String,
user: User.fromJson(json['user'] as Map<String, dynamic>),
verified: (json['verified'] as bool?) ?? (json['user']?['verified'] as bool? ?? true),
);
}
}

View File

@@ -0,0 +1,36 @@
import 'package:flutter_dotenv/flutter_dotenv.dart';
class Favorite {
final int id;
final String mediaId;
final String mediaType;
final String title;
final String posterPath;
Favorite({
required this.id,
required this.mediaId,
required this.mediaType,
required this.title,
required this.posterPath,
});
factory Favorite.fromJson(Map<String, dynamic> json) {
return Favorite(
id: json['id'] as int? ?? 0,
mediaId: json['mediaId'] as String? ?? '',
mediaType: json['mediaType'] as String? ?? '',
title: json['title'] as String? ?? '',
posterPath: json['posterPath'] as String? ?? '',
);
}
String get fullPosterUrl {
final baseUrl = dotenv.env['API_URL']!;
if (posterPath.isEmpty) {
return '$baseUrl/images/w500/placeholder.jpg';
}
final cleanPath = posterPath.startsWith('/') ? posterPath.substring(1) : posterPath;
return '$baseUrl/images/w500/$cleanPath';
}
}

View File

@@ -0,0 +1,57 @@
class LibraryLicense {
final String name;
final String version;
final String license;
final String url;
final String description;
final String? licenseText;
const LibraryLicense({
required this.name,
required this.version,
required this.license,
required this.url,
required this.description,
this.licenseText,
});
Map<String, dynamic> toMap() {
return {
'name': name,
'version': version,
'license': license,
'url': url,
'description': description,
'licenseText': licenseText,
};
}
LibraryLicense copyWith({
String? name,
String? version,
String? license,
String? url,
String? description,
String? licenseText,
}) {
return LibraryLicense(
name: name ?? this.name,
version: version ?? this.version,
license: license ?? this.license,
url: url ?? this.url,
description: description ?? this.description,
licenseText: licenseText ?? this.licenseText,
);
}
factory LibraryLicense.fromMap(Map<String, dynamic> map) {
return LibraryLicense(
name: map['name'] as String? ?? '',
version: map['version'] as String? ?? '',
license: map['license'] as String? ?? '',
url: map['url'] as String? ?? '',
description: map['description'] as String? ?? '',
licenseText: map['licenseText'] as String?,
);
}
}

100
lib/data/models/movie.dart Normal file
View File

@@ -0,0 +1,100 @@
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:hive/hive.dart';
part 'movie.g.dart';
@HiveType(typeId: 0)
class Movie extends HiveObject {
@HiveField(0)
final String id;
@HiveField(1)
final String title;
@HiveField(2)
final String? posterPath;
@HiveField(3)
final String? overview;
@HiveField(4)
final DateTime? releaseDate;
@HiveField(5)
final List<String>? genres;
@HiveField(6)
final double? voteAverage;
// Поле популярности из API (TMDB-style)
@HiveField(9)
final double popularity;
@HiveField(7)
final int? runtime;
// TV specific
@HiveField(10)
final int? seasonsCount;
@HiveField(11)
final int? episodesCount;
@HiveField(8)
final String? tagline;
// not stored in Hive, runtime-only field
final String mediaType;
Movie({
required this.id,
required this.title,
this.posterPath,
this.overview,
this.releaseDate,
this.genres,
this.voteAverage,
this.popularity = 0.0,
this.runtime,
this.seasonsCount,
this.episodesCount,
this.tagline,
this.mediaType = 'movie',
});
factory Movie.fromJson(Map<String, dynamic> json) {
return Movie(
id: (json['id'] as num).toString(), // Ensure id is a string
title: (json['title'] ?? json['name'] ?? '') as String,
posterPath: json['poster_path'] as String?,
overview: json['overview'] as String?,
releaseDate: json['release_date'] != null && json['release_date'].isNotEmpty
? DateTime.tryParse(json['release_date'] as String)
: json['first_air_date'] != null && json['first_air_date'].isNotEmpty
? DateTime.tryParse(json['first_air_date'] as String)
: null,
genres: List<String>.from(json['genres']?.map((g) => g['name']) ?? []),
voteAverage: (json['vote_average'] as num?)?.toDouble() ?? 0.0,
popularity: (json['popularity'] as num?)?.toDouble() ?? 0.0,
runtime: json['runtime'] is num
? (json['runtime'] as num).toInt()
: (json['episode_run_time'] is List && (json['episode_run_time'] as List).isNotEmpty)
? ((json['episode_run_time'] as List).first as num).toInt()
: null,
seasonsCount: json['number_of_seasons'] as int?,
episodesCount: json['number_of_episodes'] as int?,
tagline: json['tagline'] as String?,
mediaType: (json['media_type'] ?? (json['title'] != null ? 'movie' : 'tv')) as String,
);
}
String get fullPosterUrl {
final baseUrl = dotenv.env['API_URL']!;
if (posterPath == null || posterPath!.isEmpty) {
// Use the placeholder from our own backend
return '$baseUrl/images/w500/placeholder.jpg';
}
// Null check is already performed above, so we can use `!`
final cleanPath = posterPath!.startsWith('/') ? posterPath!.substring(1) : posterPath!;
return '$baseUrl/images/w500/$cleanPath';
}
}

View File

@@ -0,0 +1,59 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'movie.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class MovieAdapter extends TypeAdapter<Movie> {
@override
final int typeId = 0;
@override
Movie read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return Movie(
id: fields[0] as String,
title: fields[1] as String,
posterPath: fields[2] as String?,
overview: fields[3] as String?,
releaseDate: fields[4] as DateTime?,
genres: (fields[5] as List?)?.cast<String>(),
voteAverage: fields[6] as double?,
);
}
@override
void write(BinaryWriter writer, Movie obj) {
writer
..writeByte(7)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.title)
..writeByte(2)
..write(obj.posterPath)
..writeByte(3)
..write(obj.overview)
..writeByte(4)
..write(obj.releaseDate)
..writeByte(5)
..write(obj.genres)
..writeByte(6)
..write(obj.voteAverage);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MovieAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,21 @@
import 'package:hive/hive.dart';
part 'movie_preview.g.dart';
@HiveType(typeId: 1) // Use a new typeId to avoid conflicts with Movie
class MoviePreview extends HiveObject {
@HiveField(0)
final String id;
@HiveField(1)
final String title;
@HiveField(2)
final String? posterPath;
MoviePreview({
required this.id,
required this.title,
this.posterPath,
});
}

View File

@@ -0,0 +1,47 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'movie_preview.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class MoviePreviewAdapter extends TypeAdapter<MoviePreview> {
@override
final int typeId = 1;
@override
MoviePreview read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return MoviePreview(
id: fields[0] as String,
title: fields[1] as String,
posterPath: fields[2] as String?,
);
}
@override
void write(BinaryWriter writer, MoviePreview obj) {
writer
..writeByte(3)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.title)
..writeByte(2)
..write(obj.posterPath);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MoviePreviewAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,80 @@
import 'package:equatable/equatable.dart';
enum VideoSourceType {
lumex,
alloha,
}
class VideoSource extends Equatable {
final String id;
final String name;
final VideoSourceType type;
final bool isActive;
const VideoSource({
required this.id,
required this.name,
required this.type,
this.isActive = true,
});
// Default sources
static final List<VideoSource> defaultSources = [
const VideoSource(
id: 'alloha',
name: 'Alloha',
type: VideoSourceType.alloha,
isActive: true,
),
const VideoSource(
id: 'lumex',
name: 'Lumex',
type: VideoSourceType.lumex,
isActive: false,
),
];
@override
List<Object?> get props => [id, name, type, isActive];
@override
bool get stringify => true;
// Convert to map for serialization
Map<String, dynamic> toMap() {
return {
'id': id,
'name': name,
'type': type.name,
'isActive': isActive,
};
}
// Create from map for deserialization
factory VideoSource.fromMap(Map<String, dynamic> map) {
return VideoSource(
id: map['id'] as String? ?? 'unknown',
name: map['name'] as String? ?? 'Unknown',
type: VideoSourceType.values.firstWhere(
(e) => e.name == map['type'],
orElse: () => VideoSourceType.lumex,
),
isActive: map['isActive'] as bool? ?? true,
);
}
// Copy with method for immutability
VideoSource copyWith({
String? id,
String? name,
VideoSourceType? type,
bool? isActive,
}) {
return VideoSource(
id: id ?? this.id,
name: name ?? this.name,
type: type ?? this.type,
isActive: isActive ?? this.isActive,
);
}
}

View File

@@ -0,0 +1,25 @@
class Reaction {
final String type;
final int count;
Reaction({required this.type, required this.count});
factory Reaction.fromJson(Map<String, dynamic> json) {
return Reaction(
type: json['type'] as String? ?? '',
count: json['count'] as int? ?? 0,
);
}
}
class UserReaction {
final String? reactionType;
UserReaction({this.reactionType});
factory UserReaction.fromJson(Map<String, dynamic> json) {
return UserReaction(
reactionType: json['type'] as String?,
);
}
}

15
lib/data/models/user.dart Normal file
View File

@@ -0,0 +1,15 @@
class User {
final String id;
final String name;
final String email;
User({required this.id, required this.name, required this.email});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['_id'] as String? ?? '',
name: json['name'] as String? ?? '',
email: json['email'] as String? ?? '',
);
}
}

View File

@@ -0,0 +1,73 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:neomovies_mobile/data/api/api_client.dart';
import 'package:neomovies_mobile/data/models/user.dart';
import 'package:neomovies_mobile/data/services/secure_storage_service.dart';
import 'package:neomovies_mobile/data/exceptions/auth_exceptions.dart';
class AuthRepository {
final ApiClient _apiClient;
final SecureStorageService _storageService;
AuthRepository({
required ApiClient apiClient,
required SecureStorageService storageService,
}) : _apiClient = apiClient,
_storageService = storageService;
Future<void> login(String email, String password) async {
final response = await _apiClient.login(email, password);
if (!response.verified) {
throw UnverifiedAccountException(email, message: 'Account not verified');
}
await _storageService.saveToken(response.token);
await _storageService.saveUserData(
name: response.user.name,
email: response.user.email,
);
}
Future<void> register(String name, String email, String password) async {
// Registration does not automatically log in the user in this flow.
// It sends a verification code.
await _apiClient.register(name, email, password);
}
Future<void> verifyEmail(String email, String code) async {
await _apiClient.verify(email, code);
// After successful verification, the user should log in.
}
Future<void> resendVerificationCode(String email) async {
await _apiClient.resendCode(email);
}
Future<void> logout() async {
await _storageService.deleteAll();
}
Future<void> deleteAccount() async {
// The AuthenticatedHttpClient will handle the token.
await _apiClient.deleteAccount();
await _storageService.deleteAll();
}
Future<bool> isLoggedIn() async {
final token = await _storageService.getToken();
return token != null;
}
Future<User?> getCurrentUser() async {
final isLoggedIn = await this.isLoggedIn();
if (!isLoggedIn) return null;
final userData = await _storageService.getUserData();
if (userData['name'] == null || userData['email'] == null) {
return null;
}
// The User model requires an ID, which we don't have in storage.
// For the profile screen, we only need name and email.
// We'll create a User object with a placeholder ID.
return User(id: 'local', name: userData['name']!, email: userData['email']!);
}
}

View File

@@ -0,0 +1,20 @@
import 'package:neomovies_mobile/data/api/api_client.dart';
import 'package:neomovies_mobile/data/models/favorite.dart';
class FavoritesRepository {
final ApiClient _apiClient;
FavoritesRepository(this._apiClient);
Future<List<Favorite>> getFavorites() async {
return await _apiClient.getFavorites();
}
Future<void> addFavorite(String mediaId, String mediaType, String title, String posterPath) async {
await _apiClient.addFavorite(mediaId, mediaType, title, posterPath);
}
Future<void> removeFavorite(String mediaId) async {
await _apiClient.removeFavorite(mediaId);
}
}

View File

@@ -0,0 +1,33 @@
import 'package:neomovies_mobile/data/api/api_client.dart';
import 'package:neomovies_mobile/data/models/movie.dart';
import 'package:neomovies_mobile/data/models/movie_preview.dart';
class MovieRepository {
final ApiClient _apiClient;
MovieRepository({required ApiClient apiClient}) : _apiClient = apiClient;
Future<List<Movie>> getPopularMovies({int page = 1}) async {
return _apiClient.getPopularMovies(page: page);
}
Future<List<Movie>> getTopRatedMovies({int page = 1}) async {
return _apiClient.getTopRatedMovies(page: page);
}
Future<List<Movie>> getUpcomingMovies({int page = 1}) async {
return _apiClient.getUpcomingMovies(page: page);
}
Future<Movie> getMovieById(String movieId) async {
return _apiClient.getMovieById(movieId);
}
Future<Movie> getTvById(String tvId) async {
return _apiClient.getTvById(tvId);
}
Future<List<Movie>> searchMovies(String query, {int page = 1}) async {
return _apiClient.searchMovies(query, page: page);
}
}

View File

@@ -0,0 +1,20 @@
import 'package:neomovies_mobile/data/api/api_client.dart';
import 'package:neomovies_mobile/data/models/reaction.dart';
class ReactionsRepository {
final ApiClient _apiClient;
ReactionsRepository(this._apiClient);
Future<Map<String,int>> getReactionCounts(String mediaType,String mediaId) async {
return await _apiClient.getReactionCounts(mediaType, mediaId);
}
Future<UserReaction> getMyReaction(String mediaType,String mediaId) async {
return await _apiClient.getMyReaction(mediaType, mediaId);
}
Future<void> setReaction(String mediaType,String mediaId, String reactionType) async {
await _apiClient.setReaction(mediaType, mediaId, reactionType);
}
}

View File

@@ -0,0 +1,77 @@
// lib/data/services/alloha_player_service.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:neomovies_mobile/data/models/player/video_quality.dart';
import 'package:neomovies_mobile/data/models/player/audio_track.dart';
import 'package:neomovies_mobile/data/models/player/subtitle.dart';
class AllohaPlayerService {
static const String _baseUrl = 'https://neomovies.site'; // Replace with actual base URL
Future<Map<String, dynamic>> getStreamInfo(String mediaId, String mediaType) async {
try {
// First, get the player page
final response = await http.get(
Uri.parse('$_baseUrl/$mediaType/$mediaId/player'),
);
if (response.statusCode == 200) {
// Parse the response to extract stream information
return _parsePlayerPage(response.body);
} else {
throw Exception('Failed to load player page: ${response.statusCode}');
}
} catch (e) {
throw Exception('Error getting stream info: $e');
}
}
Map<String, dynamic> _parsePlayerPage(String html) {
// TODO: Implement actual HTML parsing based on the Alloha player page structure
// This is a placeholder - you'll need to update this based on the actual HTML structure
// Example structure (replace with actual parsing):
return {
'streamUrl': 'https://example.com/stream.m3u8',
'qualities': [
{'name': '1080p', 'resolution': '1920x1080', 'url': '...'},
{'name': '720p', 'resolution': '1280x720', 'url': '...'},
],
'audioTracks': [
{'id': 'ru', 'name': 'Русский', 'language': 'ru', 'isDefault': true},
{'id': 'en', 'name': 'English', 'language': 'en'},
],
'subtitles': [
{'id': 'ru', 'name': 'Русские', 'language': 'ru', 'url': '...'},
{'id': 'en', 'name': 'English', 'language': 'en', 'url': '...'},
],
};
}
// Convert parsed data to our models
List<VideoQuality> parseQualities(List<dynamic> qualities) {
return qualities.map((q) => VideoQuality(
name: q['name'],
resolution: q['resolution'],
url: q['url'],
)).toList();
}
List<AudioTrack> parseAudioTracks(List<dynamic> tracks) {
return tracks.map((t) => AudioTrack(
id: t['id'],
name: t['name'],
language: t['language'],
isDefault: t['isDefault'] ?? false,
)).toList();
}
List<Subtitle> parseSubtitles(List<dynamic> subtitles) {
return subtitles.map((s) => Subtitle(
id: s['id'],
name: s['name'],
language: s['language'],
url: s['url'],
)).toList();
}
}

View File

@@ -0,0 +1,34 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class SecureStorageService {
const SecureStorageService(this._storage);
final FlutterSecureStorage _storage;
Future<void> saveToken(String token) async {
await _storage.write(key: 'auth_token', value: token);
}
Future<String?> getToken() async {
return await _storage.read(key: 'auth_token');
}
Future<void> deleteToken() async {
await _storage.delete(key: 'auth_token');
}
Future<void> saveUserData({required String name, required String email}) async {
await _storage.write(key: 'user_name', value: name);
await _storage.write(key: 'user_email', value: email);
}
Future<Map<String, String?>> getUserData() async {
final name = await _storage.read(key: 'user_name');
final email = await _storage.read(key: 'user_email');
return {'name': name, 'email': email};
}
Future<void> deleteAll() async {
await _storage.deleteAll();
}
}

View File

@@ -0,0 +1,99 @@
import 'package:hive_flutter/hive_flutter.dart';
import 'package:neomovies_mobile/data/api/api_client.dart';
import 'package:neomovies_mobile/data/models/movie.dart';
import 'package:neomovies_mobile/data/models/movie_preview.dart';
class MovieRepository {
final ApiClient _apiClient;
static const String popularBox = 'popularMovies';
static const String topRatedBox = 'topRatedMovies';
static const String upcomingBox = 'upcomingMovies';
MovieRepository({required ApiClient apiClient}) : _apiClient = apiClient;
Future<List<Movie>> getPopularMovies({int page = 1}) async {
return _getCachedThenFetch(
boxName: popularBox,
fetch: () => _apiClient.getPopularMovies(page: page),
page: page,
);
}
Future<List<Movie>> getTopRatedMovies({int page = 1}) async {
return _getCachedThenFetch(
boxName: topRatedBox,
fetch: () => _apiClient.getTopRatedMovies(page: page),
page: page,
);
}
Future<List<Movie>> getUpcomingMovies({int page = 1}) async {
return _getCachedThenFetch(
boxName: upcomingBox,
fetch: () => _apiClient.getUpcomingMovies(page: page),
page: page,
);
}
Future<Movie> getMovieDetails(String id) async {
// Caching for movie details can be added later if needed.
return _apiClient.getMovieById(id);
}
Future<List<Movie>> _getCachedThenFetch({
required String boxName,
required Future<List<Movie>> Function() fetch,
required int page,
}) async {
final box = await Hive.openBox<MoviePreview>(boxName);
if (page == 1 && box.isNotEmpty) {
final cachedPreviews = box.values.toList();
// Convert cached previews to full Movie objects for the UI
final cachedMovies = cachedPreviews
.map((p) => Movie(id: p.id, title: p.title, posterPath: p.posterPath))
.toList();
// Fetch new data in the background but don't wait for it here
_fetchAndCache(box, fetch, page);
return cachedMovies;
}
// If no cache or not the first page, fetch from network
final networkMovies = await _fetchAndCache(box, fetch, page);
return networkMovies;
}
Future<List<Movie>> _fetchAndCache(
Box<MoviePreview> box,
Future<List<Movie>> Function() fetch,
int page,
) async {
try {
final networkMovies = await fetch();
if (page == 1) {
await box.clear();
for (var movie in networkMovies) {
// Save the lightweight preview version to the cache
final preview = MoviePreview(
id: movie.id,
title: movie.title,
posterPath: movie.posterPath,
);
await box.put(preview.id, preview);
}
}
return networkMovies;
} catch (e) {
if (page == 1 && box.isNotEmpty) {
// If network fails, return data from cache
final cachedPreviews = box.values.toList();
return cachedPreviews
.map((p) => Movie(id: p.id, title: p.title, posterPath: p.posterPath))
.toList();
}
rethrow;
}
}
}

140
lib/main.dart Normal file
View File

@@ -0,0 +1,140 @@
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http;
import 'package:neomovies_mobile/data/api/api_client.dart';
import 'package:neomovies_mobile/data/api/authenticated_http_client.dart';
import 'package:neomovies_mobile/data/models/movie.dart';
import 'package:neomovies_mobile/data/models/movie_preview.dart';
import 'package:neomovies_mobile/data/repositories/auth_repository.dart';
import 'package:neomovies_mobile/data/repositories/favorites_repository.dart';
import 'package:neomovies_mobile/data/repositories/movie_repository.dart';
import 'package:neomovies_mobile/data/repositories/reactions_repository.dart';
import 'package:neomovies_mobile/data/services/secure_storage_service.dart';
import 'package:neomovies_mobile/presentation/providers/auth_provider.dart';
import 'package:neomovies_mobile/presentation/providers/favorites_provider.dart';
import 'package:neomovies_mobile/presentation/providers/home_provider.dart';
import 'package:neomovies_mobile/presentation/providers/movie_detail_provider.dart';
import 'package:neomovies_mobile/presentation/providers/reactions_provider.dart';
import 'package:neomovies_mobile/presentation/screens/main_screen.dart';
import 'package:provider/provider.dart';
Future<void> main() async {
// Ensure widgets are initialized
WidgetsFlutterBinding.ensureInitialized();
// Load environment variables
await dotenv.load(fileName: ".env");
// Initialize Hive for local caching
await Hive.initFlutter();
// Register Adapters
Hive.registerAdapter(MovieAdapter());
Hive.registerAdapter(MoviePreviewAdapter());
runApp(
MultiProvider(
providers: [
// Core Services & Clients
Provider<FlutterSecureStorage>(create: (_) => const FlutterSecureStorage()),
Provider<SecureStorageService>(
create: (context) => SecureStorageService(context.read<FlutterSecureStorage>()),
),
Provider<http.Client>(create: (_) => http.Client()),
Provider<AuthenticatedHttpClient>(
create: (context) => AuthenticatedHttpClient(
context.read<SecureStorageService>(),
context.read<http.Client>(),
),
),
Provider<ApiClient>(
create: (context) => ApiClient(context.read<AuthenticatedHttpClient>()),
),
// Repositories
Provider<MovieRepository>(
create: (context) => MovieRepository(apiClient: context.read<ApiClient>()),
),
Provider<AuthRepository>(
create: (context) => AuthRepository(
apiClient: context.read<ApiClient>(),
storageService: context.read<SecureStorageService>(),
),
),
Provider<FavoritesRepository>(
create: (context) => FavoritesRepository(context.read<ApiClient>()),
),
Provider<ReactionsRepository>(
create: (context) => ReactionsRepository(context.read<ApiClient>()),
),
// State Notifiers (Providers)
ChangeNotifierProvider<AuthProvider>(
create: (context) => AuthProvider(authRepository: context.read<AuthRepository>()),
),
ChangeNotifierProvider<HomeProvider>(
create: (context) => HomeProvider(movieRepository: context.read<MovieRepository>())..init(),
),
ChangeNotifierProvider<MovieDetailProvider>(
create: (context) => MovieDetailProvider(
context.read<MovieRepository>(),
context.read<ApiClient>(),
),
),
ChangeNotifierProvider<ReactionsProvider>(
create: (context) => ReactionsProvider(
context.read<ReactionsRepository>(),
context.read<AuthProvider>(),
)),
ChangeNotifierProxyProvider<AuthProvider, FavoritesProvider>(
create: (context) => FavoritesProvider(
context.read<FavoritesRepository>(),
context.read<AuthProvider>(),
),
update: (context, auth, previous) => previous!..update(auth),
),
],
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
static final _defaultLightColorScheme = ColorScheme.fromSeed(seedColor: Colors.blue);
static final _defaultDarkColorScheme = ColorScheme.fromSeed(seedColor: Colors.blue, brightness: Brightness.dark);
@override
Widget build(BuildContext context) {
// Use dynamic_color to get colors from the system
return DynamicColorBuilder(
builder: (lightColorScheme, darkColorScheme) {
return MaterialApp(
title: 'NeoMovies',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: lightColorScheme ?? _defaultLightColorScheme,
useMaterial3: true,
textTheme: GoogleFonts.manropeTextTheme(
ThemeData(brightness: Brightness.light).textTheme,
),
),
darkTheme: ThemeData(
colorScheme: darkColorScheme ?? _defaultDarkColorScheme,
useMaterial3: true,
textTheme: GoogleFonts.manropeTextTheme(
ThemeData(brightness: Brightness.dark).textTheme,
),
),
themeMode: ThemeMode.system,
home: const MainScreen(),
);
},
);
}
}

View File

@@ -0,0 +1,135 @@
import 'package:flutter/material.dart';
import 'package:neomovies_mobile/data/models/user.dart';
import 'package:neomovies_mobile/data/repositories/auth_repository.dart';
import 'package:neomovies_mobile/data/exceptions/auth_exceptions.dart';
enum AuthState { initial, loading, authenticated, unauthenticated, error }
class AuthProvider extends ChangeNotifier {
AuthProvider({required AuthRepository authRepository})
: _authRepository = authRepository;
final AuthRepository _authRepository;
AuthState _state = AuthState.initial;
AuthState get state => _state;
String? _token;
String? get token => _token;
// Считаем пользователя аутентифицированным, если состояние AuthState.authenticated
bool get isAuthenticated => _state == AuthState.authenticated;
User? _user;
User? get user => _user;
String? _error;
String? get error => _error;
bool _needsVerification = false;
bool get needsVerification => _needsVerification;
String? _pendingEmail;
String? get pendingEmail => _pendingEmail;
Future<void> checkAuthStatus() async {
_state = AuthState.loading;
notifyListeners();
try {
final isLoggedIn = await _authRepository.isLoggedIn();
if (isLoggedIn) {
_user = await _authRepository.getCurrentUser();
_state = AuthState.authenticated;
} else {
_state = AuthState.unauthenticated;
}
} catch (e) {
_state = AuthState.unauthenticated;
}
notifyListeners();
}
Future<void> login(String email, String password) async {
_state = AuthState.loading;
_error = null;
_needsVerification = false;
notifyListeners();
try {
await _authRepository.login(email, password);
_user = await _authRepository.getCurrentUser();
_state = AuthState.authenticated;
} catch (e) {
if (e is UnverifiedAccountException) {
// Need verification flow
_needsVerification = true;
_pendingEmail = e.email;
_state = AuthState.unauthenticated;
} else {
_error = e.toString();
_state = AuthState.error;
}
}
notifyListeners();
}
Future<void> register(String name, String email, String password) async {
_state = AuthState.loading;
_error = null;
notifyListeners();
try {
await _authRepository.register(name, email, password);
// After registration, user needs to verify, so we go to unauthenticated state
// The UI will navigate to the verify screen
_state = AuthState.unauthenticated;
} catch (e) {
_error = e.toString();
_state = AuthState.error;
}
notifyListeners();
}
Future<void> verifyEmail(String email, String code) async {
_state = AuthState.loading;
_error = null;
notifyListeners();
try {
await _authRepository.verifyEmail(email, code);
// After verification, user should log in.
// For a better UX, we could auto-login them, but for now, we'll just go to the unauthenticated state.
_state = AuthState.unauthenticated;
} catch (e) {
_error = e.toString();
_state = AuthState.error;
}
notifyListeners();
}
Future<void> logout() async {
_state = AuthState.loading;
notifyListeners();
await _authRepository.logout();
_user = null;
_state = AuthState.unauthenticated;
notifyListeners();
}
Future<void> deleteAccount() async {
_state = AuthState.loading;
notifyListeners();
try {
await _authRepository.deleteAccount();
_user = null;
_state = AuthState.unauthenticated;
} catch (e) {
_error = e.toString();
_state = AuthState.error;
}
notifyListeners();
}
/// Reset pending verification state after navigating to VerifyScreen
void clearVerificationFlag() {
_needsVerification = false;
_pendingEmail = null;
notifyListeners();
}
}

View File

@@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
import 'package:neomovies_mobile/data/models/favorite.dart';
import 'package:neomovies_mobile/data/models/movie.dart';
import 'package:neomovies_mobile/data/repositories/favorites_repository.dart';
import 'package:neomovies_mobile/presentation/providers/auth_provider.dart';
class FavoritesProvider extends ChangeNotifier {
final FavoritesRepository _favoritesRepository;
AuthProvider _authProvider;
List<Favorite> _favorites = [];
bool _isLoading = false;
String? _error;
List<Favorite> get favorites => _favorites;
bool get isLoading => _isLoading;
String? get error => _error;
FavoritesProvider(this._favoritesRepository, this._authProvider) {
// Listen for authentication state changes
_authProvider.addListener(_onAuthStateChanged);
_onAuthStateChanged();
}
void update(AuthProvider authProvider) {
// Remove listener from previous AuthProvider to avoid leaks
_authProvider.removeListener(_onAuthStateChanged);
_authProvider = authProvider;
_authProvider.addListener(_onAuthStateChanged);
_onAuthStateChanged();
}
void _onAuthStateChanged() {
if (_authProvider.isAuthenticated) {
fetchFavorites();
} else {
_clearFavorites();
}
}
Future<void> fetchFavorites() async {
_isLoading = true;
_error = null;
notifyListeners();
try {
_favorites = await _favoritesRepository.getFavorites();
} catch (e) {
_error = e.toString();
} finally {
_isLoading = false;
notifyListeners();
}
}
Future<void> addFavorite(Movie movie) async {
try {
await _favoritesRepository.addFavorite(
movie.id.toString(),
'movie', // Assuming mediaType is 'movie'
movie.title,
movie.posterPath ?? '',
);
await fetchFavorites(); // Refresh the list
} catch (e) {
_error = e.toString();
notifyListeners();
}
}
Future<void> removeFavorite(String mediaId) async {
try {
await _favoritesRepository.removeFavorite(mediaId);
_favorites.removeWhere((fav) => fav.mediaId == mediaId);
notifyListeners();
} catch (e) {
_error = e.toString();
notifyListeners();
}
}
bool isFavorite(String mediaId) {
return _favorites.any((fav) => fav.mediaId == mediaId);
}
void _clearFavorites() {
_favorites = [];
_error = null;
_isLoading = false;
notifyListeners();
}
}

View File

@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:neomovies_mobile/data/models/movie.dart';
import 'package:neomovies_mobile/data/repositories/movie_repository.dart';
enum ViewState { idle, loading, success, error }
class HomeProvider extends ChangeNotifier {
final MovieRepository _movieRepository;
HomeProvider({required MovieRepository movieRepository})
: _movieRepository = movieRepository;
List<Movie> _popularMovies = [];
List<Movie> get popularMovies => _popularMovies;
List<Movie> _topRatedMovies = [];
List<Movie> get topRatedMovies => _topRatedMovies;
List<Movie> _upcomingMovies = [];
List<Movie> get upcomingMovies => _upcomingMovies;
bool _isLoading = false;
bool get isLoading => _isLoading;
String? _errorMessage;
String? get errorMessage => _errorMessage;
// Initial fetch
void init() {
fetchAllMovies();
}
Future<void> fetchAllMovies() async {
_isLoading = true;
_errorMessage = null;
// Notify listeners only for the initial loading state
if (_popularMovies.isEmpty) {
notifyListeners();
}
try {
final results = await Future.wait([
_movieRepository.getPopularMovies(),
_movieRepository.getTopRatedMovies(),
_movieRepository.getUpcomingMovies(),
]);
_popularMovies = results[0];
_topRatedMovies = results[1];
_upcomingMovies = results[2];
} catch (e) {
_errorMessage = 'Failed to fetch movies: ${e.toString()}';
} finally {
_isLoading = false;
notifyListeners();
}
}
}

View File

@@ -0,0 +1,254 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:yaml/yaml.dart';
import '../../data/models/library_license.dart';
const Map<String, String> _licenseOverrides = {
'archive': 'MIT',
'args': 'BSD-3-Clause',
'async': 'BSD-3-Clause',
'boolean_selector': 'BSD-3-Clause',
'characters': 'BSD-3-Clause',
'clock': 'Apache-2.0',
'collection': 'BSD-3-Clause',
'convert': 'BSD-3-Clause',
'crypto': 'BSD-3-Clause',
'cupertino_icons': 'MIT',
'dbus': 'MIT',
'fake_async': 'Apache-2.0',
'file': 'Apache-2.0',
'flutter_lints': 'BSD-3-Clause',
'flutter_secure_storage_linux': 'BSD-3-Clause',
'flutter_secure_storage_macos': 'BSD-3-Clause',
'flutter_secure_storage_platform_interface': 'BSD-3-Clause',
'flutter_secure_storage_web': 'BSD-3-Clause',
'flutter_secure_storage_windows': 'BSD-3-Clause',
'http_parser': 'BSD-3-Clause',
'intl': 'BSD-3-Clause',
'js': 'BSD-3-Clause',
'leak_tracker': 'BSD-3-Clause',
'lints': 'BSD-3-Clause',
'matcher': 'BSD-3-Clause',
'material_color_utilities': 'BSD-3-Clause',
'meta': 'BSD-3-Clause',
'petitparser': 'MIT',
'platform': 'BSD-3-Clause',
'plugin_platform_interface': 'BSD-3-Clause',
'pool': 'BSD-3-Clause',
'posix': 'MIT',
'source_span': 'BSD-3-Clause',
'stack_trace': 'BSD-3-Clause',
'stream_channel': 'BSD-3-Clause',
'string_scanner': 'BSD-3-Clause',
'term_glyph': 'BSD-3-Clause',
'test_api': 'BSD-3-Clause',
'typed_data': 'BSD-3-Clause',
'uuid': 'MIT',
'vector_math': 'BSD-3-Clause',
'vm_service': 'BSD-3-Clause',
'win32': 'BSD-3-Clause',
'xdg_directories': 'MIT',
'xml': 'MIT',
'yaml': 'MIT',
};
class LicensesProvider with ChangeNotifier {
final ValueNotifier<List<LibraryLicense>> _licenses = ValueNotifier([]);
final ValueNotifier<bool> _isLoading = ValueNotifier(false);
final ValueNotifier<String?> _error = ValueNotifier(null);
LicensesProvider() {
loadLicenses();
}
ValueNotifier<List<LibraryLicense>> get licenses => _licenses;
ValueNotifier<bool> get isLoading => _isLoading;
ValueNotifier<String?> get error => _error;
Future<void> loadLicenses({bool forceRefresh = false}) async {
_isLoading.value = true;
_error.value = null;
try {
final cachedLicenses = await _loadFromCache();
if (cachedLicenses != null && !forceRefresh) {
_licenses.value = cachedLicenses;
// Still trigger background update for licenses that were loading or failed
final toUpdate = cachedLicenses.where((l) => l.license == 'loading...' || l.license == 'unknown').toList();
if (toUpdate.isNotEmpty) {
_fetchFullLicenseInfo(toUpdate);
}
} else {
_licenses.value = await _fetchInitialLicenses();
_fetchFullLicenseInfo(_licenses.value.where((l) => l.license == 'loading...').toList());
}
} catch (e) {
_error.value = 'Failed to load licenses: $e';
}
_isLoading.value = false;
}
Future<List<LibraryLicense>?> _loadFromCache() async {
try {
final prefs = await SharedPreferences.getInstance();
final jsonStr = prefs.getString('licenses_cache');
if (jsonStr != null) {
final List<dynamic> jsonList = jsonDecode(jsonStr);
return jsonList.map((e) => LibraryLicense.fromMap(e)).toList();
}
} catch (_) {}
return null;
}
Future<List<LibraryLicense>> _fetchInitialLicenses() async {
final result = <LibraryLicense>[];
try {
final lockFileContent = await rootBundle.loadString('pubspec.lock');
final doc = loadYaml(lockFileContent);
final packages = doc['packages'] as YamlMap;
final pubspecContent = await rootBundle.loadString('pubspec.yaml');
final pubspec = loadYaml(pubspecContent);
result.add(LibraryLicense(
name: pubspec['name'],
version: pubspec['version'],
license: 'Apache 2.0',
url: 'https://gitlab.com/foxixius/neomovies_mobile',
description: pubspec['description'],
));
for (final key in packages.keys) {
final name = key.toString();
final package = packages[key];
if (package['source'] != 'hosted') continue;
final version = package['version'].toString();
result.add(LibraryLicense(
name: name,
version: version,
license: 'loading...',
url: 'https://pub.dev/packages/$name',
description: '',
));
}
} catch (e) {
_error.value = 'Failed to load initial license list: $e';
}
return result;
}
void _fetchFullLicenseInfo(List<LibraryLicense> toFetch) async {
final futures = toFetch.map((lib) async {
try {
final url = 'https://pub.dev/api/packages/${lib.name}';
final resp = await http.get(Uri.parse(url)).timeout(const Duration(seconds: 5));
if (resp.statusCode == 200) {
final data = jsonDecode(resp.body) as Map<String, dynamic>;
final pubspec = data['latest']['pubspec'] as Map<String, dynamic>;
String licenseType = (pubspec['license'] ?? 'unknown').toString();
if (licenseType == 'unknown' && _licenseOverrides.containsKey(lib.name)) {
licenseType = _licenseOverrides[lib.name]!;
}
final repoUrl = (pubspec['repository'] ?? pubspec['homepage'] ?? 'https://pub.dev/packages/${lib.name}').toString();
final description = (pubspec['description'] ?? '').toString();
return lib.copyWith(license: licenseType, url: repoUrl, description: description);
}
} catch (_) {}
return lib.copyWith(license: 'unknown');
}).toList();
final updatedLicenses = await Future.wait(futures);
final currentList = List<LibraryLicense>.from(_licenses.value);
bool hasChanged = false;
for (final updated in updatedLicenses) {
final index = currentList.indexWhere((e) => e.name == updated.name);
if (index != -1 && currentList[index].license != updated.license) {
currentList[index] = updated;
hasChanged = true;
}
}
if (hasChanged) {
_licenses.value = currentList;
_saveToCache(currentList);
}
}
Future<String> fetchLicenseText(LibraryLicense library) async {
if (library.licenseText != null) return library.licenseText!;
final cached = (await _loadFromCache())?.firstWhere((e) => e.name == library.name, orElse: () => library);
if (cached?.licenseText != null) {
return cached!.licenseText!;
}
try {
final text = await _fetchLicenseTextFromRepo(library.url);
if (text != null) {
final updatedLibrary = library.copyWith(licenseText: text);
final currentList = List<LibraryLicense>.from(_licenses.value);
final index = currentList.indexWhere((e) => e.name == library.name);
if (index != -1) {
currentList[index] = updatedLibrary;
_licenses.value = currentList;
_saveToCache(currentList);
}
return text;
}
} catch (_) {}
return library.license;
}
Future<String?> _fetchLicenseTextFromRepo(String repoUrl) async {
try {
final uri = Uri.parse(repoUrl);
final segments = uri.pathSegments.where((s) => s.isNotEmpty).toList();
if (segments.length < 2) return null;
final author = segments[0];
final repo = segments[1].replaceAll('.git', '');
final branches = ['main', 'master', 'HEAD']; // Common branch names
final filenames = ['LICENSE', 'LICENSE.md', 'LICENSE.txt', 'LICENSE-2.0.txt']; // Common license filenames
String? rawUrlBase;
if (repoUrl.contains('github.com')) {
rawUrlBase = 'https://raw.githubusercontent.com/$author/$repo';
} else if (repoUrl.contains('gitlab.com')) {
rawUrlBase = 'https://gitlab.com/$author/$repo/-/raw';
} else {
return null; // Unsupported provider
}
for (final branch in branches) {
for (final filename in filenames) {
final url = '$rawUrlBase/$branch/$filename';
try {
final resp = await http.get(Uri.parse(url)).timeout(const Duration(seconds: 5));
if (resp.statusCode == 200 && resp.body.isNotEmpty) {
return resp.body;
}
} catch (_) {
// Ignore timeout or other errors and try next candidate
}
}
}
} catch (_) {}
return null;
}
Future<void> _saveToCache(List<LibraryLicense> licenses) async {
try {
final prefs = await SharedPreferences.getInstance();
final jsonStr = jsonEncode(licenses.map((e) => e.toMap()).toList());
await prefs.setString('licenses_cache_v2', jsonStr);
} catch (_) {}
}
}

Some files were not shown because too many files have changed in this diff Show More