├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .metadata
├── .vscode
└── launch.json
├── LICENSE
├── README.md
├── analysis_options.yaml
├── android
├── .gitignore
├── app
│ ├── build.gradle
│ └── src
│ │ ├── debug
│ │ └── AndroidManifest.xml
│ │ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── kotlin
│ │ │ └── com
│ │ │ │ └── example
│ │ │ │ └── odin
│ │ │ │ └── MainActivity.kt
│ │ └── res
│ │ │ ├── drawable-v21
│ │ │ ├── app_icon_odintv.png
│ │ │ └── launch_background.xml
│ │ │ ├── drawable
│ │ │ ├── app_icon_odintv.png
│ │ │ └── launch_background.xml
│ │ │ ├── mipmap-hdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-mdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── values-night
│ │ │ └── styles.xml
│ │ │ └── values
│ │ │ └── styles.xml
│ │ └── profile
│ │ └── AndroidManifest.xml
├── build.gradle
├── gradle.properties
├── gradle
│ └── wrapper
│ │ └── gradle-wrapper.properties
└── settings.gradle
├── assets
└── images
│ ├── imdb.png
│ ├── logo.svg
│ ├── realdebrid.png
│ ├── tmdb.png
│ └── trakt.png
├── imdb_result.json
├── install_firetv.sh
├── ios
├── .gitignore
├── Flutter
│ ├── AppFrameworkInfo.plist
│ ├── Debug.xcconfig
│ └── Release.xcconfig
├── Runner.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ └── WorkspaceSettings.xcsettings
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── Runner.xcscheme
├── Runner.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── WorkspaceSettings.xcsettings
└── Runner
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── Icon-App-1024x1024@1x.png
│ │ ├── Icon-App-20x20@1x.png
│ │ ├── Icon-App-20x20@2x.png
│ │ ├── Icon-App-20x20@3x.png
│ │ ├── Icon-App-29x29@1x.png
│ │ ├── Icon-App-29x29@2x.png
│ │ ├── Icon-App-29x29@3x.png
│ │ ├── Icon-App-40x40@1x.png
│ │ ├── Icon-App-40x40@2x.png
│ │ ├── Icon-App-40x40@3x.png
│ │ ├── Icon-App-60x60@2x.png
│ │ ├── Icon-App-60x60@3x.png
│ │ ├── Icon-App-76x76@1x.png
│ │ ├── Icon-App-76x76@2x.png
│ │ └── Icon-App-83.5x83.5@2x.png
│ └── LaunchImage.imageset
│ │ ├── Contents.json
│ │ ├── LaunchImage.png
│ │ ├── LaunchImage@2x.png
│ │ ├── LaunchImage@3x.png
│ │ └── README.md
│ ├── Base.lproj
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
│ ├── Info.plist
│ └── Runner-Bridging-Header.h
├── lib
├── controllers
│ ├── app_controller.dart
│ ├── base_controller.dart
│ ├── detail_controller.dart
│ ├── grid_controller.dart
│ ├── home_controller.dart
│ ├── settings_controller.dart
│ ├── streams_controller.dart
│ └── watched_controller.dart
├── data
│ ├── entities
│ │ ├── config.dart
│ │ ├── imdb.dart
│ │ ├── imdb.g.dart
│ │ ├── realdebrid.dart
│ │ ├── realdebrid.g.dart
│ │ ├── scrape.dart
│ │ ├── scrape.g.dart
│ │ ├── tmdb.dart
│ │ ├── tmdb.g.dart
│ │ ├── trakt.dart
│ │ ├── trakt.g.dart
│ │ └── user.dart
│ ├── models
│ │ ├── auth_model.dart
│ │ ├── item_model.dart
│ │ └── settings_model.dart
│ └── services
│ │ ├── api.dart
│ │ ├── db.dart
│ │ ├── imdb_service.dart
│ │ ├── mqtt.dart
│ │ ├── scrape_service.dart
│ │ ├── tmdb_service.dart
│ │ └── trakt_service.dart
├── helpers.dart
├── main.dart
├── main.reflectable.dart
├── theme.dart
└── ui
│ ├── app.dart
│ ├── appbar.dart
│ ├── cover
│ ├── animated.dart
│ ├── backdrop_cover.dart
│ ├── poster_cover.dart
│ └── still_cover.dart
│ ├── detail
│ ├── detail.dart
│ ├── detail_movie.dart
│ ├── detail_show.dart
│ ├── episodes.dart
│ └── widgets.dart
│ ├── dialogs
│ ├── default.dart
│ └── streams.dart
│ ├── focusnodes.dart
│ ├── login.dart
│ ├── pages
│ ├── grid.dart
│ └── home.dart
│ ├── selectbuttonfixer.dart
│ ├── settings.dart
│ ├── userselect.dart
│ └── widgets
│ ├── buttons.dart
│ ├── carousel.dart
│ ├── ensure_visible.dart
│ ├── loaders.dart
│ ├── section.dart
│ └── widgets.dart
├── pubspec.lock
├── pubspec.yaml
├── screenshots
├── btc_donation.png
├── connect.png
├── odin-tv.png
└── odin-tv2.png
├── test
├── widget_test.dart
└── widget_test.reflectable.dart
└── web
├── favicon.png
├── icons
├── Icon-192.png
├── Icon-512.png
├── Icon-maskable-192.png
└── Icon-maskable-512.png
├── index.html
└── manifest.json
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI - Build and push
2 |
3 | on:
4 | push:
5 | tags:
6 | - "*"
7 |
8 | concurrency:
9 | group: ${{ github.workflow }}
10 | cancel-in-progress: true
11 |
12 | jobs:
13 | build-and-push:
14 | name: Build and push
15 | runs-on: ubuntu-latest
16 | steps:
17 | - name: Checkout
18 | uses: actions/checkout@v4
19 | - name: Log in to the Container registry
20 | uses: docker/login-action@v3
21 | with:
22 | registry: ghcr.io
23 | username: ${{ github.actor }}
24 | password: ${{ secrets.GITHUB_TOKEN }}
25 | - name: Clone repository
26 | uses: actions/checkout@v4
27 | - uses: actions/setup-java@v4
28 | with:
29 | distribution: "oracle"
30 | java-version: "17"
31 | - name: Set up Flutter
32 | uses: subosito/flutter-action@v2
33 | with:
34 | channel: stable
35 | cache: true
36 | - run: flutter build apk
37 | - name: "Rename"
38 | run: mv build/app/outputs/apk/release/app-release.apk build/app/outputs/apk/release/odin-tv_${{ github.ref_name }}.apk
39 | - name: "Add to release"
40 | uses: softprops/action-gh-release@v2
41 | with:
42 | files: |
43 | build/app/outputs/apk/release/odin-tv_${{ github.ref_name }}.apk
44 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Miscellaneous
3 | *.class
4 | *.log
5 | *.pyc
6 | *.swp
7 | .DS_Store
8 | .atom/
9 | .buildlog/
10 | .history
11 | .svn/
12 |
13 | # IntelliJ related
14 | *.iml
15 | *.ipr
16 | *.iws
17 | .idea/
18 |
19 | # The .vscode folder contains launch configuration and tasks you configure in
20 | # VS Code which you may wish to be included in version control, so this line
21 | # is commented out by default.
22 | #.vscode/
23 |
24 | # Flutter/Dart/Pub related
25 | **/doc/api/
26 | **/ios/Flutter/.last_build_id
27 | .dart_tool/
28 | .flutter-plugins
29 | .flutter-plugins-dependencies
30 | .packages
31 | .pub-cache/
32 | .pub/
33 | /build/
34 |
35 | # Web related
36 |
37 | # Symbolication related
38 | app.*.symbols
39 |
40 | # Obfuscation related
41 | app.*.map.json
42 |
43 | # Android Studio will place build artifacts here
44 | /android/app/debug
45 | /android/app/profile
46 | /android/app/release
47 |
--------------------------------------------------------------------------------
/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision: "2663184aa79047d0a33a14a3b607954f8fdd8730"
8 | channel: "stable"
9 |
10 | project_type: app
11 |
12 | # Tracks metadata for the flutter migrate command
13 | migration:
14 | platforms:
15 | - platform: root
16 | create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
17 | base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
18 | - platform: web
19 | create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
20 | base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
21 |
22 | # User provided section
23 |
24 | # List of Local paths (relative to this file) that should be
25 | # ignored by the migrate tool.
26 | #
27 | # Files that are not part of the templates will be ignored by default.
28 | unmanaged_files:
29 | - 'lib/main.dart'
30 | - 'ios/Runner.xcodeproj/project.pbxproj'
31 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "odin (debug)",
6 | "request": "launch",
7 | "type": "dart",
8 | "flutterMode": "debug"
9 | },
10 | {
11 | "name": "odin (profile mode)",
12 | "request": "launch",
13 | "type": "dart",
14 | "flutterMode": "profile"
15 | }
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | 
3 |
4 |
5 | Android TV app for Odin
6 |
7 | 
8 | []()
9 | []()
10 |
11 | 
12 | 
13 |
14 | This is the Android TV app to enjoy Odin on your TV.
15 |
16 | # Installation
17 |
18 | - Install and setup the [Odin server](https://github.com/ad-on-is/odin-server) first
19 | - Download the latest APK from the [release](https://github.com/ad-on-is/odin-tv/releases) page
20 | - Sideload to your Android TV box
21 | - Follow the on-screen instructions
22 |
23 | 
24 |
25 | ## Compatible devices
26 |
27 | - Xiaomi MiBox
28 | - Amazon Fire TV Cube 2/3
29 | - Other devices need testing and confirmation
30 |
31 | > [!WARNING]
32 | >
33 | > The app does not come with a built-in video player. See below.
34 |
35 | >
36 |
37 | ## Video Player
38 |
39 | Odin TV **does not include a video player**, since there are better players out there that support audio-passthrough, etc, it is not worth the hassle to reinvent the wheel.
40 |
41 | Recommended players:
42 |
43 | - Just (default)
44 | - Nova
45 | - Kodi
46 | - MX Player
47 | - VLC
48 |
49 | # Known issues
50 |
51 | - The navigation is a bit wonky. This is due to Flutters lack of keyboard and D-Pad navigation. I'm working my ass of to make this as smooth as possible.
52 | - If you experience issues, just smash the buttons on your remote.
53 |
54 | # Donations
55 |
56 |
57 |
58 | ## License
59 |
60 | GPLv3
61 |
62 | ---
63 |
64 | > GitHub [@ad-on-is](https://github.com/ad-on-is)
65 |
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | # This file configures the analyzer, which statically analyzes Dart code to
2 | # check for errors, warnings, and lints.
3 | #
4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled
5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
6 | # invoked from the command line by running `flutter analyze`.
7 |
8 | # The following line activates a set of recommended lints for Flutter apps,
9 | # packages, and plugins designed to encourage good coding practices.
10 | include: package:flutter_lints/flutter.yaml
11 |
12 | linter:
13 | # The lint rules applied to this project can be customized in the
14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml`
15 | # included above or to enable additional rules. A list of all available lints
16 | # and their documentation is published at
17 | # https://dart-lang.github.io/linter/lints/index.html.
18 | #
19 | # Instead of disabling a lint rule for the entire project in the
20 | # section below, it can also be suppressed for a single line of code
21 | # or a specific dart file by using the `// ignore: name_of_lint` and
22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file
23 | # producing the lint.
24 | rules:
25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule
26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
27 |
28 | # Additional information about this file can be found at
29 | # https://dart.dev/guides/language/analysis-options
30 | analyzer:
31 | exclude:
32 | - "**/*.g.dart"
33 | - "**/*.freezed.dart"
34 | errors:
35 | invalid_annotation_target: ignore
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | gradle-wrapper.jar
2 | /.gradle
3 | /captures/
4 | /gradlew
5 | /gradlew.bat
6 | /local.properties
7 | GeneratedPluginRegistrant.java
8 |
9 | # Remember to never publicly share your keystore.
10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
11 | key.properties
12 | **/*.keystore
13 | **/*.jks
14 |
--------------------------------------------------------------------------------
/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | def localProperties = new Properties()
2 | def localPropertiesFile = rootProject.file('local.properties')
3 | if (localPropertiesFile.exists()) {
4 | localPropertiesFile.withReader('UTF-8') { reader ->
5 | localProperties.load(reader)
6 | }
7 | }
8 |
9 | def flutterRoot = localProperties.getProperty('flutter.sdk')
10 | if (flutterRoot == null) {
11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
12 | }
13 |
14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
15 | if (flutterVersionCode == null) {
16 | flutterVersionCode = '1'
17 | }
18 |
19 | def flutterVersionName = localProperties.getProperty('flutter.versionName')
20 | if (flutterVersionName == null) {
21 | flutterVersionName = '1.0'
22 | }
23 |
24 | apply plugin: 'com.android.application'
25 | apply plugin: 'kotlin-android'
26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
27 |
28 | android {
29 | namespace "org.odintv.app"
30 | compileSdkVersion flutter.compileSdkVersion
31 |
32 | compileOptions {
33 | sourceCompatibility JavaVersion.VERSION_1_8
34 | targetCompatibility JavaVersion.VERSION_1_8
35 | }
36 |
37 | kotlinOptions {
38 | jvmTarget = '1.8'
39 | }
40 |
41 | sourceSets {
42 | main.java.srcDirs += 'src/main/kotlin'
43 | }
44 |
45 | defaultConfig {
46 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
47 | applicationId "org.odintv.app"
48 | minSdkVersion flutter.minSdkVersion
49 | targetSdkVersion flutter.targetSdkVersion
50 | versionCode flutterVersionCode.toInteger()
51 | versionName flutterVersionName
52 | }
53 |
54 | buildTypes {
55 | release {
56 | // TODO: Add your own signing config for the release build.
57 | // Signing with the debug keys for now, so `flutter run --release` works.
58 | signingConfig signingConfigs.debug
59 | }
60 | }
61 | }
62 |
63 | flutter {
64 | source '../..'
65 | }
66 |
67 | dependencies {
68 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
69 | }
70 |
--------------------------------------------------------------------------------
/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
18 |
26 |
30 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
42 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/com/example/odin/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package org.odintv.app
2 |
3 | import io.flutter.embedding.android.FlutterActivity
4 |
5 | class MainActivity: FlutterActivity() {
6 | }
7 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-v21/app_icon_odintv.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ad-on-is/odin-tv/2a84a609370b8cccdbd9ab3e0f578f63f4fa15f6/android/app/src/main/res/drawable-v21/app_icon_odintv.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-v21/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/app_icon_odintv.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ad-on-is/odin-tv/2a84a609370b8cccdbd9ab3e0f578f63f4fa15f6/android/app/src/main/res/drawable/app_icon_odintv.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ad-on-is/odin-tv/2a84a609370b8cccdbd9ab3e0f578f63f4fa15f6/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ad-on-is/odin-tv/2a84a609370b8cccdbd9ab3e0f578f63f4fa15f6/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ad-on-is/odin-tv/2a84a609370b8cccdbd9ab3e0f578f63f4fa15f6/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ad-on-is/odin-tv/2a84a609370b8cccdbd9ab3e0f578f63f4fa15f6/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ad-on-is/odin-tv/2a84a609370b8cccdbd9ab3e0f578f63f4fa15f6/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/values-night/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/profile/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext.kotlin_version = '1.9.24'
3 | repositories {
4 | google()
5 | mavenCentral()
6 | }
7 |
8 | dependencies {
9 | classpath 'com.android.tools.build:gradle:8.7.3'
10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
11 | }
12 | }
13 |
14 | allprojects {
15 | repositories {
16 | google()
17 | mavenCentral()
18 | }
19 | }
20 |
21 | rootProject.buildDir = '../build'
22 | subprojects {
23 | project.buildDir = "${rootProject.buildDir}/${project.name}"
24 | }
25 | subprojects {
26 | project.evaluationDependsOn(':app')
27 | }
28 |
29 | tasks.register("clean", Delete) {
30 | delete rootProject.buildDir
31 | }
32 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx1536M
2 | android.useAndroidX=true
3 | android.enableJetifier=true
4 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Jun 23 08:50:38 CEST 2017
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip
7 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | //include ':app'
2 | //
3 | //def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
4 | //def properties = new Properties()
5 | //
6 | //assert localPropertiesFile.exists()
7 | //localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
8 | //
9 | //def flutterSdkPath = properties.getProperty("flutter.sdk")
10 | //assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
11 | //apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
12 | //
13 |
14 | pluginManagement {
15 | def flutterSdkPath = {
16 | def properties = new Properties()
17 | file("local.properties").withInputStream { properties.load(it) }
18 | def flutterSdkPath = properties.getProperty("flutter.sdk")
19 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
20 | return flutterSdkPath
21 | }
22 | settings.ext.flutterSdkPath = flutterSdkPath()
23 |
24 | includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
25 |
26 | repositories {
27 | google()
28 | mavenCentral()
29 | gradlePluginPortal()
30 | }
31 | }
32 |
33 | plugins {
34 | id "dev.flutter.flutter-plugin-loader" version "1.0.0"
35 | id "com.android.application" version "8.7.3" apply false
36 | id "org.jetbrains.kotlin.android" version "1.9.24" apply false
37 | }
38 |
39 | include ":app"
40 |
--------------------------------------------------------------------------------
/assets/images/imdb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ad-on-is/odin-tv/2a84a609370b8cccdbd9ab3e0f578f63f4fa15f6/assets/images/imdb.png
--------------------------------------------------------------------------------
/assets/images/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
82 |
--------------------------------------------------------------------------------
/assets/images/realdebrid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ad-on-is/odin-tv/2a84a609370b8cccdbd9ab3e0f578f63f4fa15f6/assets/images/realdebrid.png
--------------------------------------------------------------------------------
/assets/images/tmdb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ad-on-is/odin-tv/2a84a609370b8cccdbd9ab3e0f578f63f4fa15f6/assets/images/tmdb.png
--------------------------------------------------------------------------------
/assets/images/trakt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ad-on-is/odin-tv/2a84a609370b8cccdbd9ab3e0f578f63f4fa15f6/assets/images/trakt.png
--------------------------------------------------------------------------------
/imdb_result.json:
--------------------------------------------------------------------------------
1 | {
2 | "@context": "https://schema.org",
3 | "@type": "Movie",
4 | "url": "/title/tt4513678/",
5 | "name": "Ghostbusters: Afterlife",
6 | "alternateName": "Ghostbusters: Legacy",
7 | "image": "https://m.media-amazon.com/images/M/MV5BMmZiMjdlN2UtYzdiZS00YjgxLTgyZGMtYzE4ZGU5NTlkNjhhXkEyXkFqcGdeQXVyMTEyMjM2NDc2._V1_.jpg",
8 | "description": "When a single mom and her two kids arrive in a small town, they begin to discover their connection to the original Ghostbusters and the secret legacy their grandfather left behind.",
9 | "review": {
10 | "@type": "Review",
11 | "itemReviewed": { "@type": "CreativeWork", "url": "/title/tt4513678/" },
12 | "author": { "@type": "Person", "name": "samuelelegolasfilippi" },
13 | "dateCreated": "2021-11-01",
14 | "inLanguage": "English",
15 | "name": "The closing and the beginning",
16 | "reviewBody": "I saw it preview last night. A film that in some points recalls the first Gostbuster, but which has its own ideology. The characters they present to us are almost all well characterized and useful to the plot. The director did a good job, given the great responsibility he had with this film. Some ideas are very bright and innovative. The first hour and a half is very beautiful, full of jokes and suspenseful scenes, the problem is the last half hour that is lost a bit by recalling the first film a lot. The soundtrack is irrelevant, apart from those two moments when you hear the original themes. Overall it is a good film, not at the levels of the first but certainly superior to the female Gostbuster.",
17 | "reviewRating": {
18 | "@type": "Rating",
19 | "worstRating": 1,
20 | "bestRating": 10,
21 | "ratingValue": 7
22 | }
23 | },
24 | "aggregateRating": {
25 | "@type": "AggregateRating",
26 | "ratingCount": 99525,
27 | "bestRating": 10,
28 | "worstRating": 1,
29 | "ratingValue": 7.3
30 | },
31 | "contentRating": "PG-13",
32 | "genre": ["Adventure", "Comedy", "Fantasy"],
33 | "datePublished": "2021-11-19",
34 | "keywords": "sequel,ghost,ghostbusters,ghostbuster,teenager",
35 | "trailer": {
36 | "@type": "VideoObject",
37 | "name": "Official Trailer",
38 | "embedUrl": "/video/imdb/vi637322009",
39 | "thumbnail": {
40 | "@type": "ImageObject",
41 | "contentUrl": "https://m.media-amazon.com/images/M/MV5BYWFhZDk5NjEtNmU4OS00NGQyLTk2N2MtNTk5YWE5NDQ2ZjcyXkEyXkFqcGdeQWRvb2xpbmhk._V1_.jpg"
42 | },
43 | "thumbnailUrl": "https://m.media-amazon.com/images/M/MV5BYWFhZDk5NjEtNmU4OS00NGQyLTk2N2MtNTk5YWE5NDQ2ZjcyXkEyXkFqcGdeQWRvb2xpbmhk._V1_.jpg",
44 | "description": "Afterlife, when a single mom and her two kids arrive in a small town, they begin to discover their connection to the original ghostbusters and the secret legacy their grandfather left behind."
45 | },
46 | "actor": [
47 | { "@type": "Person", "url": "/name/nm4689420/", "name": "Carrie Coon" },
48 | { "@type": "Person", "url": "/name/nm0748620/", "name": "Paul Rudd" },
49 | { "@type": "Person", "url": "/name/nm6016511/", "name": "Finn Wolfhard" }
50 | ],
51 | "director": [
52 | { "@type": "Person", "url": "/name/nm0718646/", "name": "Jason Reitman" }
53 | ],
54 | "creator": [
55 | { "@type": "Organization", "url": "/company/co0050868/" },
56 | { "@type": "Organization", "url": "/company/co0309252/" },
57 | { "@type": "Organization", "url": "/company/co0526864/" },
58 | { "@type": "Person", "url": "/name/nm1481493/", "name": "Gil Kenan" },
59 | { "@type": "Person", "url": "/name/nm0718646/", "name": "Jason Reitman" },
60 | { "@type": "Person", "url": "/name/nm0000101/", "name": "Dan Aykroyd" }
61 | ],
62 | "duration": "PT2H4M"
63 | }
64 |
--------------------------------------------------------------------------------
/install_firetv.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | adb connect $(landevice Fire):5555 && adb -s $(landevice Fire):5555 install build/app/outputs/flutter-apk/app-release.apk
--------------------------------------------------------------------------------
/ios/.gitignore:
--------------------------------------------------------------------------------
1 | **/dgph
2 | *.mode1v3
3 | *.mode2v3
4 | *.moved-aside
5 | *.pbxuser
6 | *.perspectivev3
7 | **/*sync/
8 | .sconsign.dblite
9 | .tags*
10 | **/.vagrant/
11 | **/DerivedData/
12 | Icon?
13 | **/Pods/
14 | **/.symlinks/
15 | profile
16 | xcuserdata
17 | **/.generated/
18 | Flutter/App.framework
19 | Flutter/Flutter.framework
20 | Flutter/Flutter.podspec
21 | Flutter/Generated.xcconfig
22 | Flutter/ephemeral/
23 | Flutter/app.flx
24 | Flutter/app.zip
25 | Flutter/flutter_assets/
26 | Flutter/flutter_export_environment.sh
27 | ServiceDefinitions.json
28 | Runner/GeneratedPluginRegistrant.*
29 |
30 | # Exceptions to above rules.
31 | !default.mode1v3
32 | !default.mode2v3
33 | !default.pbxuser
34 | !default.perspectivev3
35 |
--------------------------------------------------------------------------------
/ios/Flutter/AppFrameworkInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | App
9 | CFBundleIdentifier
10 | io.flutter.flutter.app
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | App
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1.0
23 | MinimumOSVersion
24 | 9.0
25 |
26 |
27 |
--------------------------------------------------------------------------------
/ios/Flutter/Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include "Generated.xcconfig"
2 |
--------------------------------------------------------------------------------
/ios/Flutter/Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include "Generated.xcconfig"
2 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
41 |
42 |
52 |
54 |
60 |
61 |
62 |
63 |
69 |
71 |
77 |
78 |
79 |
80 |
82 |
83 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import Flutter
3 |
4 | @UIApplicationMain
5 | @objc class AppDelegate: FlutterAppDelegate {
6 | override func application(
7 | _ application: UIApplication,
8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
9 | ) -> Bool {
10 | GeneratedPluginRegistrant.register(with: self)
11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "Icon-App-20x20@2x.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "Icon-App-20x20@3x.png",
13 | "scale" : "3x"
14 | },
15 | {
16 | "size" : "29x29",
17 | "idiom" : "iphone",
18 | "filename" : "Icon-App-29x29@1x.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "29x29",
23 | "idiom" : "iphone",
24 | "filename" : "Icon-App-29x29@2x.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "29x29",
29 | "idiom" : "iphone",
30 | "filename" : "Icon-App-29x29@3x.png",
31 | "scale" : "3x"
32 | },
33 | {
34 | "size" : "40x40",
35 | "idiom" : "iphone",
36 | "filename" : "Icon-App-40x40@2x.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "40x40",
41 | "idiom" : "iphone",
42 | "filename" : "Icon-App-40x40@3x.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "60x60",
47 | "idiom" : "iphone",
48 | "filename" : "Icon-App-60x60@2x.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "60x60",
53 | "idiom" : "iphone",
54 | "filename" : "Icon-App-60x60@3x.png",
55 | "scale" : "3x"
56 | },
57 | {
58 | "size" : "20x20",
59 | "idiom" : "ipad",
60 | "filename" : "Icon-App-20x20@1x.png",
61 | "scale" : "1x"
62 | },
63 | {
64 | "size" : "20x20",
65 | "idiom" : "ipad",
66 | "filename" : "Icon-App-20x20@2x.png",
67 | "scale" : "2x"
68 | },
69 | {
70 | "size" : "29x29",
71 | "idiom" : "ipad",
72 | "filename" : "Icon-App-29x29@1x.png",
73 | "scale" : "1x"
74 | },
75 | {
76 | "size" : "29x29",
77 | "idiom" : "ipad",
78 | "filename" : "Icon-App-29x29@2x.png",
79 | "scale" : "2x"
80 | },
81 | {
82 | "size" : "40x40",
83 | "idiom" : "ipad",
84 | "filename" : "Icon-App-40x40@1x.png",
85 | "scale" : "1x"
86 | },
87 | {
88 | "size" : "40x40",
89 | "idiom" : "ipad",
90 | "filename" : "Icon-App-40x40@2x.png",
91 | "scale" : "2x"
92 | },
93 | {
94 | "size" : "76x76",
95 | "idiom" : "ipad",
96 | "filename" : "Icon-App-76x76@1x.png",
97 | "scale" : "1x"
98 | },
99 | {
100 | "size" : "76x76",
101 | "idiom" : "ipad",
102 | "filename" : "Icon-App-76x76@2x.png",
103 | "scale" : "2x"
104 | },
105 | {
106 | "size" : "83.5x83.5",
107 | "idiom" : "ipad",
108 | "filename" : "Icon-App-83.5x83.5@2x.png",
109 | "scale" : "2x"
110 | },
111 | {
112 | "size" : "1024x1024",
113 | "idiom" : "ios-marketing",
114 | "filename" : "Icon-App-1024x1024@1x.png",
115 | "scale" : "1x"
116 | }
117 | ],
118 | "info" : {
119 | "version" : 1,
120 | "author" : "xcode"
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ad-on-is/odin-tv/2a84a609370b8cccdbd9ab3e0f578f63f4fa15f6/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ad-on-is/odin-tv/2a84a609370b8cccdbd9ab3e0f578f63f4fa15f6/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ad-on-is/odin-tv/2a84a609370b8cccdbd9ab3e0f578f63f4fa15f6/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ad-on-is/odin-tv/2a84a609370b8cccdbd9ab3e0f578f63f4fa15f6/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ad-on-is/odin-tv/2a84a609370b8cccdbd9ab3e0f578f63f4fa15f6/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ad-on-is/odin-tv/2a84a609370b8cccdbd9ab3e0f578f63f4fa15f6/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ad-on-is/odin-tv/2a84a609370b8cccdbd9ab3e0f578f63f4fa15f6/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ad-on-is/odin-tv/2a84a609370b8cccdbd9ab3e0f578f63f4fa15f6/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ad-on-is/odin-tv/2a84a609370b8cccdbd9ab3e0f578f63f4fa15f6/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ad-on-is/odin-tv/2a84a609370b8cccdbd9ab3e0f578f63f4fa15f6/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ad-on-is/odin-tv/2a84a609370b8cccdbd9ab3e0f578f63f4fa15f6/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ad-on-is/odin-tv/2a84a609370b8cccdbd9ab3e0f578f63f4fa15f6/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ad-on-is/odin-tv/2a84a609370b8cccdbd9ab3e0f578f63f4fa15f6/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ad-on-is/odin-tv/2a84a609370b8cccdbd9ab3e0f578f63f4fa15f6/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ad-on-is/odin-tv/2a84a609370b8cccdbd9ab3e0f578f63f4fa15f6/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "LaunchImage.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "LaunchImage@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "LaunchImage@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ad-on-is/odin-tv/2a84a609370b8cccdbd9ab3e0f578f63f4fa15f6/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ad-on-is/odin-tv/2a84a609370b8cccdbd9ab3e0f578f63f4fa15f6/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ad-on-is/odin-tv/2a84a609370b8cccdbd9ab3e0f578f63f4fa15f6/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md:
--------------------------------------------------------------------------------
1 | # Launch Screen Assets
2 |
3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory.
4 |
5 | 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.
--------------------------------------------------------------------------------
/ios/Runner/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/ios/Runner/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/ios/Runner/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | Odin
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | odin
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | $(FLUTTER_BUILD_NAME)
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | $(FLUTTER_BUILD_NUMBER)
25 | LSRequiresIPhoneOS
26 |
27 | UILaunchStoryboardName
28 | LaunchScreen
29 | UIMainStoryboardFile
30 | Main
31 | UISupportedInterfaceOrientations
32 |
33 | UIInterfaceOrientationPortrait
34 | UIInterfaceOrientationLandscapeLeft
35 | UIInterfaceOrientationLandscapeRight
36 |
37 | UISupportedInterfaceOrientations~ipad
38 |
39 | UIInterfaceOrientationPortrait
40 | UIInterfaceOrientationPortraitUpsideDown
41 | UIInterfaceOrientationLandscapeLeft
42 | UIInterfaceOrientationLandscapeRight
43 |
44 | UIViewControllerBasedStatusBarAppearance
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/ios/Runner/Runner-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | #import "GeneratedPluginRegistrant.h"
2 |
--------------------------------------------------------------------------------
/lib/controllers/app_controller.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_riverpod/flutter_riverpod.dart';
3 | import 'package:odin/data/entities/trakt.dart';
4 | import 'package:odin/ui/pages/grid.dart';
5 | import 'package:odin/ui/pages/home.dart';
6 |
7 | final List pages = [
8 | const HomePage(),
9 | const MoviesGrid(),
10 | const ShowsGrid()
11 | ];
12 |
13 | final appPageProvider = StateProvider((ref) => 0);
14 | final selectedItemProvider = StateProvider((ref) => Trakt());
15 | final selectedSectionProvider = StateProvider((ref) => "");
16 | final selectedItemOfSectionProvider = StateProviderFamily(
17 | (ref, section) => Trakt(),
18 | );
19 | final bgBusyProvider = StateProvider((ref) => false);
20 | final appBusyProvider = StateProvider((ref) => false);
21 | final bgAlpha = StateProvider((ref) => 230);
22 | final debugProvider = StateProvider((ref) => "test");
23 |
24 | final beforeFocusProvider = StateProvider((ref) => false);
25 | final beforeFocusHasFocus = StateProvider((ref) => true);
26 | final afterFocusProvider = StateProvider((ref) => false);
27 |
--------------------------------------------------------------------------------
/lib/controllers/base_controller.dart:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/lib/controllers/detail_controller.dart:
--------------------------------------------------------------------------------
1 | import 'package:android_intent_plus/android_intent.dart';
2 | import 'package:android_intent_plus/flag.dart';
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter_riverpod/flutter_riverpod.dart';
5 | import 'package:odin/data/entities/trakt.dart';
6 | import 'package:odin/data/models/auth_model.dart';
7 | import 'package:odin/data/services/mqtt.dart';
8 | import 'package:odin/data/services/trakt_service.dart';
9 | import 'package:odin/helpers.dart';
10 |
11 | import '../data/services/tmdb_service.dart';
12 |
13 | class DetailController extends StateNotifier with BaseHelper {
14 | Trakt? item;
15 | final Ref ref;
16 | int episode = 0;
17 | int season = 0;
18 | List seasons = [];
19 | final TraktService traktService;
20 | final TmdbService tmdbService;
21 | final AuthModel auth;
22 | Map> episodeImages = {};
23 | DetailController(this.ref, this.traktService, this.tmdbService, this.auth)
24 | : super(false);
25 | final FocusNode playButtonNode = FocusNode();
26 | final seasonButtonNode = FocusNode();
27 | // Worker? seasonWorker;
28 | void onInit(Trakt i) async {
29 | item = i;
30 | if (item?.type == 'movie') {
31 | logInfo("Detail: ${item?.type}");
32 | playButtonNode.requestFocus();
33 | } else {
34 | seasonButtonNode.requestFocus();
35 | logInfo("${item?.type ?? "häää"} - ${item!.title}");
36 | seasons.addAll(await traktService.getSeasons(item!.ids.trakt));
37 | logInfo(seasons.length.toString());
38 | state = !state;
39 | getEpisodeImages();
40 | }
41 | ref.read(mqttProvider).disConnectMQTT();
42 | // seasonWorker = ever(season, (_) {
43 | // getEpisodeImages(season.value);
44 | // });
45 |
46 | // state = !state;
47 | }
48 |
49 | Future getTraktSeasons() async {}
50 |
51 | void getEpisodeImages() async {
52 | episodeImages = await tmdbService.getEpisodeImages(
53 | item?.ids.tmdb ?? 0, seasons.map((s) => s.number).toList());
54 | state = !state;
55 | }
56 |
57 | Future playTrailer() async {
58 | const intent = AndroidIntent(
59 | action: 'action_view',
60 | package: "com.teamsmart.videomanager.tv",
61 | data: "https://youtube.com/watch?v=9vN6DHB6bJc",
62 | arguments: {
63 | 'force_fullscreen': 'true',
64 | 'finish_on_end': 'true',
65 | },
66 | flags: [
67 | Flag.FLAG_ACTIVITY_MULTIPLE_TASK,
68 | Flag.FLAG_ACTIVITY_NO_HISTORY,
69 | ],
70 | );
71 | await intent.launch();
72 | }
73 |
74 | String getEpisodeImage(int season, int episode) {
75 | const String url = 'https://image.tmdb.org/t/p/w185';
76 | if (episodeImages[season] != null &&
77 | episodeImages[season]![episode] != null) {
78 | return url + episodeImages[season]![episode]!;
79 | }
80 | return url;
81 | }
82 |
83 | void onClose() {
84 | // seasonWorker?.dispose();
85 | // super.onClose();
86 | }
87 |
88 | void setSeason(int s) {
89 | season = s;
90 | episode = 0;
91 | state = !state;
92 | }
93 |
94 | void setEpisode(int e) async {
95 | episode = e;
96 | state = !state;
97 | }
98 | }
99 |
100 | final detailController = StateNotifierProvider.autoDispose((ref) =>
101 | DetailController(ref, ref.watch(traktProvider), ref.watch(tmdbProvider),
102 | ref.watch(authProvider.notifier)));
103 |
104 | final selectedSeasonProvider = StateProvider((ref) => 0);
105 |
--------------------------------------------------------------------------------
/lib/controllers/grid_controller.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_riverpod/flutter_riverpod.dart';
2 | import 'package:odin/data/models/item_model.dart';
3 | import 'package:odin/data/services/api.dart';
4 | import 'package:odin/data/services/trakt_service.dart';
5 |
6 | final gridSectionProvider = FutureProvider.family
7 | .autoDispose, String>((ref, type) async {
8 | ref.watch(watchedProvider);
9 | final api = ref.watch(apiProvider);
10 | final u = await api.get('/-/user');
11 |
12 | if (u.isLeft()) {
13 | return [];
14 | }
15 |
16 | return List.from(
17 | u.getRight().fold(() => null, (r) => r)["trakt_sections"][type])
18 | .map((e) => SectionItem(
19 | title: e["title"],
20 | url: e["url"],
21 | paginate: e["paginate"] ?? false,
22 | big: e["big"] ?? false))
23 | .toList()
24 | ..add(SectionItem(title: 'GENRE', url: '', type: type, isGenre: true))
25 | ..toList();
26 |
27 | // var year = DateTime.now().year;
28 | // final sections = [
29 | // SectionItem(
30 | // title: 'MOST WATCHED TODAY',
31 | // url: '/$type/watched/daily',
32 | // big: true,
33 | // ),
34 | // SectionItem(
35 | // title: 'POPULAR $year/${year - 1} RELEASES',
36 | // url: '/$type/watched/weekly?years=$year,${year - 1}',
37 | // big: true,
38 | // ),
39 | // ];
40 | // if (type == 'movies') {
41 | // sections.add(SectionItem(
42 | // title: 'BOX OFFICE',
43 | // url: '/$type/boxoffice',
44 | // paginate: false,
45 | // ));
46 | // }
47 |
48 | // if (type == 'shows') {
49 | // sections.add(SectionItem(
50 | // title: 'MOST POPULAR',
51 | // url: '/$type/popular',
52 | // ));
53 | // }
54 |
55 | // sections.add(SectionItem(
56 | // title: 'YOUR WATCHLIST',
57 | // url: '/sync/watchlist/$type/title',
58 | // ));
59 |
60 | // sections.add(
61 | // SectionItem(
62 | // title: 'HIGHLY ANTICIPATED',
63 | // url: '/$type/anticipated',
64 | // ),
65 | // );
66 | // return sections;
67 | });
68 |
--------------------------------------------------------------------------------
/lib/controllers/home_controller.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_riverpod/flutter_riverpod.dart';
2 | import 'package:odin/data/models/item_model.dart';
3 | import 'package:odin/data/services/api.dart';
4 | import 'package:odin/data/services/trakt_service.dart';
5 |
6 | final homeSectionProvider =
7 | FutureProvider.autoDispose>((ref) async {
8 | ref.watch(watchedProvider);
9 | final api = ref.watch(apiProvider);
10 | final u = await api.get('/-/user');
11 |
12 | if (u.isLeft()) {
13 | return [];
14 | }
15 |
16 | return List.from(
17 | u.getRight().fold(() => null, (r) => r)["trakt_sections"]["home"])
18 | .map((e) => SectionItem(
19 | title: e["title"],
20 | url: e["url"],
21 | paginate: e["paginate"] ?? false,
22 | big: e["big"] ?? false))
23 | .toList();
24 | });
25 |
--------------------------------------------------------------------------------
/lib/controllers/settings_controller.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_riverpod/flutter_riverpod.dart';
2 | import 'package:odin/data/entities/config.dart';
3 | import 'package:odin/data/models/settings_model.dart';
4 |
5 | class SettingsController extends StateNotifier {
6 | final Ref ref;
7 |
8 | SettingsModel settings;
9 | Config get config => settings.config;
10 |
11 | SettingsController(this.ref, this.settings) : super(false) {
12 | // auth = ref!.watch(authProvider);
13 | }
14 |
15 | void save() {
16 | settings.save();
17 | // state = !state;
18 | }
19 | }
20 |
21 | final settingsController = StateNotifierProvider(
22 | (ref) => SettingsController(ref, ref.watch(settingsProvider)));
23 |
--------------------------------------------------------------------------------
/lib/controllers/streams_controller.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter_riverpod/flutter_riverpod.dart';
5 | import 'package:android_intent_plus/android_intent.dart';
6 | import 'package:helpers/helpers.dart';
7 | import 'package:mqtt_client/mqtt_client.dart';
8 | import 'package:odin/controllers/detail_controller.dart';
9 | import 'package:odin/data/entities/scrape.dart';
10 | import 'package:odin/data/entities/trakt.dart';
11 | import 'package:odin/data/models/settings_model.dart';
12 | import 'package:odin/data/services/api.dart';
13 | import 'package:odin/data/services/mqtt.dart';
14 | import 'package:odin/data/services/scrape_service.dart';
15 | import 'package:odin/data/services/trakt_service.dart';
16 | import 'package:odin/helpers.dart';
17 | import 'package:odin/ui/dialogs/default.dart';
18 |
19 | class StreamUrl {
20 | String filename;
21 | int filesize;
22 | String download;
23 | List info;
24 | String quality;
25 |
26 | StreamUrl(
27 | this.filename, this.filesize, this.download, this.info, this.quality);
28 | }
29 |
30 | class StreamsController extends StateNotifier with BaseHelper {
31 | ScrapeService scrapeService;
32 | ApiService api;
33 | MQTTService mqtt;
34 | TraktService traktService;
35 | DetailController detail;
36 | SettingsModel settings;
37 | Map get player => settings.player;
38 | final Ref ref;
39 | bool inited = false;
40 |
41 | Trakt? item;
42 | Trakt? show;
43 | Trakt? season;
44 | bool confirmed = false;
45 | String status = "Scraping";
46 |
47 | String playerTitle = '';
48 |
49 | Map> scrapes = {
50 | 'HDR': [],
51 | '4K': [],
52 | '1080p': [],
53 | '720p': [],
54 | 'SD': [],
55 | 'CAM': []
56 | };
57 |
58 | StreamsController(this.ref, this.api, this.mqtt, this.scrapeService,
59 | this.traktService, this.settings, this.detail)
60 | : super(false);
61 |
62 | void init(Trakt item, Trakt? show, Trakt? season) async {
63 | inited = true;
64 | this.item = item;
65 | this.show = show;
66 | this.season = season;
67 | if (item.type == 'movie') {
68 | playerTitle = '${item.title} (${item.year})';
69 | } else {
70 | playerTitle =
71 | '${show?.title} (${show?.year}) - S${season?.number}E${item.number} - ${item.title}';
72 | }
73 |
74 | getUrls();
75 | }
76 |
77 | // test
78 |
79 | DateTime? startPlay;
80 | DateTime? endPlay;
81 |
82 | Future confirmProbablyWatched(int percent, BuildContext ctx) async {
83 | return await showDialog(
84 | context: ctx,
85 | builder: (dctx) => DefaultDialog(
86 | child: Padding(
87 | padding: const EdgeInsets.all(20.0),
88 | child: Column(
89 | mainAxisSize: MainAxisSize.min,
90 | children: [
91 | Headline3('You only watched $percent%'),
92 | const BodyText1('Do you want to mark it as watched?'),
93 | const SizedBox(height: 20),
94 | Row(
95 | mainAxisSize: MainAxisSize.min,
96 | children: [
97 | TextButton(
98 | child: const BodyText1('Yes, mark as watched'),
99 | onPressed: () {
100 | confirmed = true;
101 | Navigator.of(dctx).pop();
102 | },
103 | ),
104 | const SizedBox(width: 20),
105 | TextButton(
106 | child: const BodyText1('No, I will watch later'),
107 | onPressed: () {
108 | confirmed = false;
109 | Navigator.of(dctx).pop();
110 | },
111 | )
112 | ],
113 | )
114 | ],
115 | ),
116 | ),
117 | ));
118 | }
119 |
120 | // // invoked when coming back from player
121 | void checkInTrakt(BuildContext ctx) async {
122 | if (startPlay == null) return;
123 |
124 | endPlay = DateTime.now();
125 | final duration = endPlay!.difference(startPlay!);
126 | double p = 100 / item!.runtime * duration.inMinutes;
127 | int progress = p.round();
128 | startPlay = null;
129 | if (progress < 0) progress = 0;
130 | if (progress >= 75) {
131 | progress = 100;
132 | traktService.setWatched(item: item!, show: show, season: season);
133 | } else {
134 | await confirmProbablyWatched(progress, ctx);
135 | if (confirmed) {
136 | progress = 100;
137 | traktService.setWatched(item: item!, show: show, season: season);
138 | }
139 | }
140 | traktService.watching(item!, progress, 'stop');
141 | detail.state = !detail.state;
142 | }
143 |
144 | void getUrls({bool cache = true}) async {
145 | logWarning("getting Urls");
146 | String topic = 'odin-movieshow/movie/${item!.ids.trakt}';
147 | if (show != null) {
148 | topic = 'odin-movieshow/episode/${item!.ids.trakt}';
149 | }
150 | await mqtt.initializeMQTTClient(topic);
151 | mqtt.client.updates!.listen((dynamic t) {
152 | final MqttPublishMessage recMess = t[0].payload;
153 | final message =
154 | MqttPublishPayload.bytesToStringAsString(recMess.payload.message);
155 | if (message == "SCRAPING_DONE") {
156 | status = "Done";
157 | state = !state;
158 | return;
159 | }
160 | final s = Scrape.fromJson(json.decode(message));
161 | String q = s.quality;
162 | if (q == '4K' && (s.info.contains('HDR') || s.info.contains('DV'))) {
163 | q = 'HDR';
164 | }
165 | // logInfo(s.title);
166 | scrapes[q]!.add(s);
167 | state = !state;
168 | });
169 |
170 | await scrapeService.scrape(
171 | item: item!, show: show, season: season, doCache: cache);
172 |
173 | await Future.delayed(const Duration(seconds: 1));
174 | state = !state;
175 | }
176 |
177 | List allUrls() {
178 | List all = [];
179 |
180 | scrapes.forEach((q, values) {
181 | for (var s in values) {
182 | for (var link in s.links) {
183 | var su = StreamUrl(
184 | link["filename"], link["filesize"], link["download"], s.info, q);
185 | all.add(su);
186 | }
187 | }
188 | });
189 |
190 | return all;
191 | }
192 |
193 | void openPlayer(String url) async {
194 | _launchPlayer(url);
195 | }
196 |
197 | void _startWatching() {
198 | startPlay = DateTime.now();
199 | traktService.watching(item!, 0, 'start');
200 | }
201 |
202 | void _launchPlayer(String url) async {
203 | final intent = AndroidIntent(
204 | action: 'action_view',
205 | package: player['id'], // com.mxtech.videoplayer.pro
206 | type: 'video/*',
207 | data: url,
208 | arguments: {'title': playerTitle},
209 | );
210 | _startWatching();
211 | await intent.launch();
212 | }
213 | }
214 |
215 | final streamsController = StateNotifierProvider.autoDispose((ref) {
216 | final mqtt = ref.watch(mqttProvider);
217 | ref.onDispose(() {
218 | mqtt.client.disconnect();
219 | });
220 | return StreamsController(
221 | ref,
222 | ref.watch(apiProvider),
223 | mqtt,
224 | ref.watch(scrapeProvider),
225 | ref.watch(traktProvider),
226 | ref.watch(settingsProvider),
227 | ref.watch(detailController.notifier));
228 | });
229 |
--------------------------------------------------------------------------------
/lib/controllers/watched_controller.dart:
--------------------------------------------------------------------------------
1 |
2 |
3 | class WatchedController {
4 | // LazyBox hiveWatched = Get.find(tag: 'hive-watched');
5 | // Rx watched = false.obs;
6 | // final Key key;
7 | // String boxkey = '';
8 | // Worker? listener;
9 |
10 | // WatchedController(this.key) {
11 | // boxkey = key.toString().replaceAll("[<'", "").replaceAll("'>]", "");
12 | // }
13 |
14 | // @override
15 | // void onInit() {
16 | // listener = ever(watched, (_) {
17 | // update();
18 | // });
19 | // getWatched();
20 | // super.onInit();
21 | // }
22 |
23 | // void getWatched() async {
24 | // watched(await hiveWatched.get(boxkey));
25 | // }
26 |
27 | // @override
28 | // void onClose() {
29 | // listener?.dispose();
30 | // super.onClose();
31 | // }
32 | }
33 |
--------------------------------------------------------------------------------
/lib/data/entities/config.dart:
--------------------------------------------------------------------------------
1 | class Config {
2 | String player;
3 | bool scrobble;
4 | Config({this.player = 'Just', this.scrobble = true});
5 | }
6 |
--------------------------------------------------------------------------------
/lib/data/entities/imdb.dart:
--------------------------------------------------------------------------------
1 | import 'package:json_annotation/json_annotation.dart';
2 | part 'imdb.g.dart';
3 |
4 | @JsonSerializable()
5 | class Imdb {
6 | double rating;
7 | int votes;
8 | double lastRating;
9 | String lastComment;
10 | String thumbUrl;
11 |
12 | Imdb(
13 | {this.rating = 0,
14 | this.votes = 0,
15 | this.lastRating = 0,
16 | this.lastComment = '',
17 | this.thumbUrl = ''});
18 |
19 | factory Imdb.fromJson(Map json) {
20 | return Imdb(
21 | rating: json['aggregateRating'] != null
22 | ? json['aggregateRating']['ratingValue'] * 1.0
23 | : 0.0,
24 | votes: json['aggregateRating'] != null
25 | ? json['aggregateRating']['ratingCount']
26 | : 0,
27 | thumbUrl:
28 | json['trailer'] != null ? json['trailer']['thumbnailUrl'] : '',
29 | lastComment:
30 | json['review'] != null && json['review']['reviewBody'] != null
31 | ? json['review']['reviewBody']
32 | : '',
33 | lastRating:
34 | json['review'] != null && json['review']['reviewRating'] != null
35 | ? json['review']['reviewRating']['ratingValue'] * 1.0
36 | : 0.0);
37 | }
38 |
39 | Map toJson() => _$ImdbToJson(this);
40 | }
41 |
--------------------------------------------------------------------------------
/lib/data/entities/imdb.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'imdb.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | Imdb _$ImdbFromJson(Map json) => Imdb(
10 | rating: (json['rating'] as num?)?.toDouble() ?? 0,
11 | votes: json['votes'] as int? ?? 0,
12 | lastRating: (json['lastRating'] as num?)?.toDouble() ?? 0,
13 | lastComment: json['lastComment'] as String? ?? '',
14 | thumbUrl: json['thumbUrl'] as String? ?? '',
15 | );
16 |
17 | Map _$ImdbToJson(Imdb instance) => {
18 | 'rating': instance.rating,
19 | 'votes': instance.votes,
20 | 'lastRating': instance.lastRating,
21 | 'lastComment': instance.lastComment,
22 | 'thumbUrl': instance.thumbUrl,
23 | };
24 |
--------------------------------------------------------------------------------
/lib/data/entities/realdebrid.dart:
--------------------------------------------------------------------------------
1 | import 'package:json_annotation/json_annotation.dart';
2 |
3 | part 'realdebrid.g.dart';
4 |
5 | @JsonSerializable()
6 | class RealDebrid {
7 | String id;
8 | String filename;
9 | int filesize;
10 | String download;
11 | List info;
12 |
13 | RealDebrid(
14 | {this.id = '',
15 | this.filename = '',
16 | this.filesize = 0,
17 | this.download = '',
18 | this.info = const []});
19 |
20 | factory RealDebrid.fromJson(Map json) =>
21 | _$RealDebridFromJson(json);
22 | }
23 |
--------------------------------------------------------------------------------
/lib/data/entities/realdebrid.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'realdebrid.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | RealDebrid _$RealDebridFromJson(Map json) => RealDebrid(
10 | id: json['id'] as String? ?? '',
11 | filename: json['filename'] as String? ?? '',
12 | filesize: json['filesize'] as int? ?? 0,
13 | download: json['download'] as String? ?? '',
14 | info:
15 | (json['info'] as List?)?.map((e) => e as String).toList() ??
16 | const [],
17 | );
18 |
19 | Map _$RealDebridToJson(RealDebrid instance) =>
20 | {
21 | 'id': instance.id,
22 | 'filename': instance.filename,
23 | 'filesize': instance.filesize,
24 | 'download': instance.download,
25 | 'info': instance.info,
26 | };
27 |
--------------------------------------------------------------------------------
/lib/data/entities/scrape.dart:
--------------------------------------------------------------------------------
1 | import 'package:json_annotation/json_annotation.dart';
2 | part 'scrape.g.dart';
3 |
4 | @JsonSerializable()
5 | class Scrape {
6 | String url;
7 | String magnet;
8 | @JsonKey(name: 'release_title')
9 | String title;
10 | @JsonKey(name: 'scraper')
11 | @JsonKey(defaultValue: 'unknown')
12 | String scraper;
13 | int size;
14 | String quality;
15 | List info;
16 | List links;
17 |
18 | Scrape(
19 | {this.url = '',
20 | this.magnet = '',
21 | this.title = '',
22 | this.scraper = '',
23 | this.size = 0,
24 | this.quality = '',
25 | this.links = const [],
26 | this.info = const []});
27 |
28 | factory Scrape.fromJson(Map json) => _$ScrapeFromJson(json);
29 | }
30 |
--------------------------------------------------------------------------------
/lib/data/entities/scrape.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'scrape.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | Scrape _$ScrapeFromJson(Map json) => Scrape(
10 | url: json['url'] as String? ?? '',
11 | magnet: json['magnet'] as String? ?? '',
12 | title: json['release_title'] as String? ?? '',
13 | scraper: json['scraper'] as String? ?? '',
14 | size: json['size'] as int? ?? 0,
15 | quality: json['quality'] as String? ?? '',
16 | links: json['links'] as List? ?? const [],
17 | info:
18 | (json['info'] as List?)?.map((e) => e as String).toList() ??
19 | const [],
20 | );
21 |
22 | Map _$ScrapeToJson(Scrape instance) => {
23 | 'url': instance.url,
24 | 'magnet': instance.magnet,
25 | 'release_title': instance.title,
26 | 'scraper': instance.scraper,
27 | 'size': instance.size,
28 | 'quality': instance.quality,
29 | 'info': instance.info,
30 | 'links': instance.links,
31 | };
32 |
--------------------------------------------------------------------------------
/lib/data/entities/tmdb.dart:
--------------------------------------------------------------------------------
1 | import 'package:json_annotation/json_annotation.dart';
2 |
3 | part 'tmdb.g.dart';
4 |
5 | @JsonSerializable(explicitToJson: true)
6 | class TmdbCast {
7 | final String name;
8 | @JsonKey(name: 'profile_path')
9 | final String profilePath;
10 | final String character;
11 | String get profileBig => 'https://image.tmdb.org/t/p/w780$profilePath';
12 | String get profileSmall => 'https://image.tmdb.org/t/p/w342$profilePath';
13 | TmdbCast({this.name = '', this.profilePath = '', this.character = ''});
14 | factory TmdbCast.fromJson(Map json) =>
15 | _$TmdbCastFromJson(json);
16 |
17 | Map toJson() => _$TmdbCastToJson(this);
18 | }
19 |
20 | @JsonSerializable(explicitToJson: true)
21 | class TmdbCredits {
22 | final List cast;
23 | const TmdbCredits({this.cast = const []});
24 | factory TmdbCredits.fromJson(Map json) =>
25 | _$TmdbCreditsFromJson(json);
26 | Map toJson() => _$TmdbCreditsToJson(this);
27 | }
28 |
29 | @JsonSerializable(explicitToJson: true)
30 | class TmdbProductionCompany {
31 | @JsonKey(name: 'logo_path')
32 | String logoPath;
33 | String name;
34 |
35 | TmdbProductionCompany({this.name = '', this.logoPath = ''});
36 | factory TmdbProductionCompany.fromJson(Map json) =>
37 | _$TmdbProductionCompanyFromJson(json);
38 | Map toJson() => _$TmdbProductionCompanyToJson(this);
39 | }
40 |
41 | @JsonSerializable(explicitToJson: true)
42 | class Tmdb {
43 | int id;
44 | @JsonKey(name: 'poster_path')
45 | String posterPath;
46 | @JsonKey(name: 'backdrop_path')
47 | String backdropPath;
48 |
49 | @JsonKey(name: 'logo_path')
50 | String logoPath;
51 |
52 | @JsonKey(name: 'original_title')
53 | String originalTitle;
54 |
55 | @JsonKey(name: 'stillPath')
56 | String stillPath;
57 |
58 | @JsonKey(name: 'episode_number')
59 | int episodeNumber;
60 |
61 | @JsonKey(name: 'vote_average')
62 | double voteAverage;
63 |
64 | @JsonKey(name: 'vote_count')
65 | int voteCount;
66 |
67 | @JsonKey(name: 'credits')
68 | TmdbCredits credits;
69 |
70 | List episodes;
71 |
72 | @JsonKey(name: 'production_companies')
73 | List productionCompanies;
74 |
75 | double get roundedRating => double.parse(voteAverage.toStringAsFixed(1));
76 | String get smallPath => 'https://image.tmdb.org/t/p/w342';
77 | String get posterBig => 'https://image.tmdb.org/t/p/w780$posterPath';
78 | String get posterSmall => 'https://image.tmdb.org/t/p/w342$posterPath';
79 | String get logoBig => 'https://image.tmdb.org/t/p/w780$logoPath';
80 | String get logoSmall => 'https://image.tmdb.org/t/p/w342$logoPath';
81 | String get stillBig => 'https://image.tmdb.org/t/p/w300$stillPath';
82 | String get stillSmall => 'https://image.tmdb.org/t/p/w185$stillPath';
83 | String get backdropBig => 'https://image.tmdb.org/t/p/w1280$backdropPath';
84 | String get backdropSmall => 'https://image.tmdb.org/t/p/w780$backdropPath';
85 |
86 | Tmdb(
87 | {this.posterPath = '',
88 | this.id = 0,
89 | this.backdropPath = '',
90 | this.logoPath = '',
91 | this.stillPath = '',
92 | this.episodeNumber = 0,
93 | this.voteCount = 0,
94 | this.voteAverage = 0.0,
95 | this.originalTitle = '',
96 | this.productionCompanies = const [],
97 | this.episodes = const [],
98 | this.credits = const TmdbCredits(cast: [])});
99 |
100 | factory Tmdb.fromJson(Map json) => _$TmdbFromJson(json);
101 | Map toJson() => _$TmdbToJson(this);
102 | }
103 |
--------------------------------------------------------------------------------
/lib/data/entities/tmdb.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'tmdb.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | TmdbCast _$TmdbCastFromJson(Map json) => TmdbCast(
10 | name: json['name'] as String? ?? '',
11 | profilePath: json['profile_path'] as String? ?? '',
12 | character: json['character'] as String? ?? '',
13 | );
14 |
15 | Map _$TmdbCastToJson(TmdbCast instance) => {
16 | 'name': instance.name,
17 | 'profile_path': instance.profilePath,
18 | 'character': instance.character,
19 | };
20 |
21 | TmdbCredits _$TmdbCreditsFromJson(Map json) => TmdbCredits(
22 | cast: (json['cast'] as List?)
23 | ?.map((e) => TmdbCast.fromJson(e as Map))
24 | .toList() ??
25 | const [],
26 | );
27 |
28 | Map _$TmdbCreditsToJson(TmdbCredits instance) =>
29 | {
30 | 'cast': instance.cast.map((e) => e.toJson()).toList(),
31 | };
32 |
33 | TmdbProductionCompany _$TmdbProductionCompanyFromJson(
34 | Map json) =>
35 | TmdbProductionCompany(
36 | name: json['name'] as String? ?? '',
37 | logoPath: json['logo_path'] as String? ?? '',
38 | );
39 |
40 | Map _$TmdbProductionCompanyToJson(
41 | TmdbProductionCompany instance) =>
42 | {
43 | 'logo_path': instance.logoPath,
44 | 'name': instance.name,
45 | };
46 |
47 | Tmdb _$TmdbFromJson(Map json) => Tmdb(
48 | posterPath: json['poster_path'] as String? ?? '',
49 | logoPath: json['logo_path'] as String? ?? '',
50 | id: json['id'] as int? ?? 0,
51 | backdropPath: json['backdrop_path'] as String? ?? '',
52 | stillPath: json['stillPath'] as String? ?? '',
53 | episodeNumber: json['episode_number'] as int? ?? 0,
54 | voteCount: json['vote_count'] as int? ?? 0,
55 | voteAverage: (json['vote_average'] as num?)?.toDouble() ?? 0.0,
56 | originalTitle: json['original_title'] as String? ?? '',
57 | productionCompanies: (json['production_companies'] as List?)
58 | ?.map((e) =>
59 | TmdbProductionCompany.fromJson(e as Map))
60 | .toList() ??
61 | const [],
62 | episodes: (json['episodes'] as List?)
63 | ?.map((e) => Tmdb.fromJson(e as Map))
64 | .toList() ??
65 | const [],
66 | credits: json['credits'] == null
67 | ? const TmdbCredits(cast: [])
68 | : TmdbCredits.fromJson(json['credits'] as Map),
69 | );
70 |
71 | Map _$TmdbToJson(Tmdb instance) => {
72 | 'id': instance.id,
73 | 'poster_path': instance.posterPath,
74 | 'logo_path': instance.logoPath,
75 | 'backdrop_path': instance.backdropPath,
76 | 'original_title': instance.originalTitle,
77 | 'stillPath': instance.stillPath,
78 | 'episode_number': instance.episodeNumber,
79 | 'vote_average': instance.voteAverage,
80 | 'vote_count': instance.voteCount,
81 | 'credits': instance.credits.toJson(),
82 | 'episodes': instance.episodes.map((e) => e.toJson()).toList(),
83 | 'production_companies':
84 | instance.productionCompanies.map((e) => e.toJson()).toList(),
85 | };
86 |
--------------------------------------------------------------------------------
/lib/data/entities/trakt.dart:
--------------------------------------------------------------------------------
1 | import 'package:json_annotation/json_annotation.dart';
2 | import 'package:odin/data/entities/tmdb.dart';
3 |
4 | import 'imdb.dart';
5 |
6 | part 'trakt.g.dart';
7 |
8 | @JsonSerializable(explicitToJson: true)
9 | class TraktIds {
10 | final int trakt;
11 | final String slug;
12 | final String imdb;
13 | final int tmdb;
14 | final int tvdb;
15 | const TraktIds(
16 | {this.trakt = 0,
17 | this.slug = '',
18 | this.imdb = '',
19 | this.tmdb = 0,
20 | this.tvdb = 0});
21 | factory TraktIds.fromJson(Map json) =>
22 | _$TraktIdsFromJson(json);
23 |
24 | Map toJson() => _$TraktIdsToJson(this);
25 | }
26 |
27 | @JsonSerializable(explicitToJson: true)
28 | class TraktEpisode {
29 | String title;
30 | int season;
31 | int number;
32 | bool watched;
33 | TraktEpisode(
34 | {this.title = '',
35 | this.season = 0,
36 | this.number = 0,
37 | this.watched = false});
38 |
39 | TraktEpisode setWatched(bool w) {
40 | watched = w;
41 | return this;
42 | }
43 |
44 | factory TraktEpisode.fromJson(Map json) =>
45 | _$TraktEpisodeFromJson(json);
46 | Map toJson() => _$TraktEpisodeToJson(this);
47 | }
48 |
49 | @JsonSerializable(explicitToJson: true)
50 | class Trakt {
51 | String type;
52 | TraktIds ids;
53 | String title;
54 | int year;
55 | Trakt? show;
56 | String language;
57 | double rating;
58 | int votes;
59 | List genres;
60 | String overview;
61 | int runtime;
62 | String country;
63 | String trailer;
64 | bool watched;
65 | TraktEpisode? episode;
66 | @JsonKey(name: 'seasons')
67 | List seasons;
68 | List episodes;
69 | @JsonKey(name: 'tmdb')
70 | Tmdb? tmdb;
71 | Imdb? imdb;
72 |
73 | DateTime released;
74 | String tagline;
75 |
76 | @JsonKey(name: 'first_aired')
77 | DateTime firstAired;
78 | String network;
79 | String status;
80 |
81 | int number;
82 | @JsonKey(name: 'episode_count')
83 | int episodeCount;
84 | @JsonKey(name: 'aired_episodes')
85 | int airedEpisodes;
86 | int season;
87 |
88 | bool get isMovie => type == 'movie';
89 | bool get isShow => type == 'show';
90 | bool get isSeason => type == 'season';
91 | bool get isEpisode => type == 'episode';
92 |
93 | Trakt setType(String t) {
94 | type = t;
95 | return this;
96 | }
97 |
98 | Trakt setWatched(bool w) {
99 | watched = w;
100 | return this;
101 | }
102 |
103 | double get roundedRating => double.parse(rating.toStringAsFixed(1));
104 |
105 | Trakt(
106 | {this.ids = const TraktIds(),
107 | this.type = '',
108 | this.watched = false,
109 | this.title = '',
110 | this.year = 0,
111 | this.rating = 0.0,
112 | this.votes = 0,
113 | this.genres = const [],
114 | this.country = '',
115 | this.trailer = '',
116 | this.network = '',
117 | this.status = '',
118 | this.language = '',
119 | this.overview = 'No overview',
120 | this.number = 0,
121 | this.season = 0,
122 | this.runtime = 0,
123 | this.episodeCount = 0,
124 | this.airedEpisodes = 0,
125 | this.seasons = const [],
126 | this.episodes = const [],
127 | this.show,
128 | this.tagline = '',
129 | DateTime? released,
130 | DateTime? firstAired})
131 | : released = released ?? DateTime.now(),
132 | firstAired = firstAired ?? DateTime.now();
133 |
134 | factory Trakt.fromJson(Map json) => _$TraktFromJson(json);
135 | Map toJson() => _$TraktToJson(this);
136 | }
137 |
--------------------------------------------------------------------------------
/lib/data/entities/trakt.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'trakt.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | TraktIds _$TraktIdsFromJson(Map json) => TraktIds(
10 | trakt: json['trakt'] as int? ?? 0,
11 | slug: json['slug'] as String? ?? '',
12 | imdb: json['imdb'] as String? ?? '',
13 | tmdb: json['tmdb'] as int? ?? 0,
14 | tvdb: json['tvdb'] as int? ?? 0,
15 | );
16 |
17 | Map _$TraktIdsToJson(TraktIds instance) => {
18 | 'trakt': instance.trakt,
19 | 'slug': instance.slug,
20 | 'imdb': instance.imdb,
21 | 'tmdb': instance.tmdb,
22 | 'tvdb': instance.tvdb,
23 | };
24 |
25 | TraktEpisode _$TraktEpisodeFromJson(Map json) => TraktEpisode(
26 | title: json['title'] as String? ?? '',
27 | season: json['season'] as int? ?? 0,
28 | number: json['number'] as int? ?? 0,
29 | watched: json['watched'] as bool? ?? false,
30 | );
31 |
32 | Map _$TraktEpisodeToJson(TraktEpisode instance) =>
33 | {
34 | 'title': instance.title,
35 | 'season': instance.season,
36 | 'number': instance.number,
37 | 'watched': instance.watched,
38 | };
39 |
40 | Trakt _$TraktFromJson(Map json) => Trakt(
41 | ids: json['ids'] == null
42 | ? const TraktIds()
43 | : TraktIds.fromJson(json['ids'] as Map),
44 | type: json['type'] as String? ?? '',
45 | watched: json['watched'] as bool? ?? false,
46 | title: json['title'] as String? ?? '',
47 | year: json['year'] as int? ?? 0,
48 | rating: (json['rating'] as num?)?.toDouble() ?? 0.0,
49 | votes: json['votes'] as int? ?? 0,
50 | genres: (json['genres'] as List?)
51 | ?.map((e) => e as String)
52 | .toList() ??
53 | const [],
54 | country: json['country'] as String? ?? '',
55 | trailer: json['trailer'] as String? ?? '',
56 | network: json['network'] as String? ?? '',
57 | status: json['status'] as String? ?? '',
58 | language: json['language'] as String? ?? '',
59 | overview: json['overview'] as String? ?? 'No overview',
60 | number: json['number'] as int? ?? 0,
61 | season: json['season'] as int? ?? 0,
62 | runtime: json['runtime'] as int? ?? 0,
63 | episodeCount: json['episode_count'] as int? ?? 0,
64 | airedEpisodes: json['aired_episodes'] as int? ?? 0,
65 | seasons: (json['seasons'] as List?)
66 | ?.map((e) => Trakt.fromJson(e as Map))
67 | .toList() ??
68 | const [],
69 | episodes: (json['episodes'] as List?)
70 | ?.map((e) => Trakt.fromJson(e as Map))
71 | .toList() ??
72 | const [],
73 | show: json['show'] == null
74 | ? null
75 | : Trakt.fromJson(json['show'] as Map),
76 | tagline: json['tagline'] as String? ?? '',
77 | released: json['released'] == null
78 | ? null
79 | : DateTime.parse(json['released'] as String),
80 | firstAired: json['first_aired'] == null
81 | ? null
82 | : DateTime.parse(json['first_aired'] as String),
83 | )
84 | ..episode = json['episode'] == null
85 | ? null
86 | : TraktEpisode.fromJson(json['episode'] as Map)
87 | ..tmdb = json['tmdb'] == null
88 | ? null
89 | : Tmdb.fromJson(json['tmdb'] as Map)
90 | ..imdb = json['imdb'] == null
91 | ? null
92 | : Imdb.fromJson(json['imdb'] as Map);
93 |
94 | Map _$TraktToJson(Trakt instance) => {
95 | 'type': instance.type,
96 | 'ids': instance.ids.toJson(),
97 | 'title': instance.title,
98 | 'year': instance.year,
99 | 'show': instance.show?.toJson(),
100 | 'language': instance.language,
101 | 'rating': instance.rating,
102 | 'votes': instance.votes,
103 | 'genres': instance.genres,
104 | 'overview': instance.overview,
105 | 'runtime': instance.runtime,
106 | 'country': instance.country,
107 | 'trailer': instance.trailer,
108 | 'watched': instance.watched,
109 | 'episode': instance.episode?.toJson(),
110 | 'seasons': instance.seasons.map((e) => e.toJson()).toList(),
111 | 'episodes': instance.episodes.map((e) => e.toJson()).toList(),
112 | 'tmdb': instance.tmdb?.toJson(),
113 | 'imdb': instance.imdb?.toJson(),
114 | 'released': instance.released.toIso8601String(),
115 | 'tagline': instance.tagline,
116 | 'first_aired': instance.firstAired.toIso8601String(),
117 | 'network': instance.network,
118 | 'status': instance.status,
119 | 'number': instance.number,
120 | 'episode_count': instance.episodeCount,
121 | 'aired_episodes': instance.airedEpisodes,
122 | 'season': instance.season,
123 | };
124 |
--------------------------------------------------------------------------------
/lib/data/entities/user.dart:
--------------------------------------------------------------------------------
1 | class User {
2 | String username;
3 | String name;
4 | String avatar;
5 | bool vip;
6 | String id;
7 | DateTime? expiration;
8 |
9 | User(
10 | {this.username = '',
11 | this.name = '',
12 | this.id = '',
13 | this.vip = false,
14 | this.avatar = '',
15 | this.expiration});
16 |
17 | Map toJSon() => {
18 | 'name': name,
19 | 'username': username,
20 | 'vip': vip,
21 | 'id': id,
22 | 'avatar': avatar,
23 | };
24 |
25 | factory User.fromTrakt(dynamic json) {
26 | return User(
27 | id: json['user']['ids']['uuid'] ?? '',
28 | username: json['user']['username'] ?? '',
29 | name: json['user']['name'] ?? '',
30 | vip: json['user']['vip'] ?? false,
31 | avatar: json['user']['images']['avatar']['full'] ?? '');
32 | }
33 | factory User.fromRealDebrid(dynamic json) {
34 | return User(
35 | username: json['username'] ?? '',
36 | avatar: json['avatar'] ?? '',
37 | vip: json['type'] == 'premium',
38 | expiration: DateTime.parse(json['expiration']));
39 | }
40 | factory User.fromJson(dynamic json) {
41 | return User(
42 | name: json['name'],
43 | id: json['id'],
44 | username: json['username'],
45 | avatar: json['avatar'],
46 | vip: json['vip']);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/lib/data/models/auth_model.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:convert';
3 |
4 | import 'package:flutter_riverpod/flutter_riverpod.dart';
5 | import 'package:odin/data/services/api.dart';
6 | import 'package:odin/data/services/db.dart';
7 | import 'package:odin/helpers.dart';
8 | import 'package:uuid/v4.dart';
9 | import 'package:web_socket_channel/web_socket_channel.dart';
10 |
11 | enum AuthState { ok, login, error, multiple, add, select }
12 |
13 | class ValidationStatus {
14 | final int status;
15 | final dynamic user;
16 | ValidationStatus(this.status, this.user);
17 | }
18 |
19 | class AuthObject {
20 | final String device;
21 | final String url;
22 | final dynamic user;
23 |
24 | AuthObject(this.device, this.url, this.user);
25 | }
26 |
27 | class AuthModel extends StateNotifier with BaseHelper {
28 | DB db;
29 | ValidationService validation;
30 | final Ref ref;
31 | String code = "...";
32 | AuthObject? me;
33 | AuthModel(this.ref, this.db, this.validation) : super(AuthState.error);
34 |
35 | Future check({bool showSelect = false}) async {
36 | final allcreds = await getAllCreds();
37 | if (allcreds.isEmpty) {
38 | login();
39 | return;
40 | }
41 | if (allcreds.length > 1 || showSelect) {
42 | state = AuthState.multiple;
43 | return;
44 | }
45 |
46 | if (await verify(allcreds[0]) == false) {
47 | return;
48 | }
49 | me = allcreds[0];
50 | state = AuthState.ok;
51 | }
52 |
53 | Future newUser() async {
54 | login();
55 | }
56 |
57 | switchUser() {
58 | state = AuthState.multiple;
59 | }
60 |
61 | Future selectUser(AuthObject creds) async {
62 | logInfo("Selecting");
63 | if (!await verify(creds)) {
64 | return;
65 | }
66 | logInfo("Should be here");
67 | me = creds;
68 | state = AuthState.ok;
69 | }
70 |
71 | Future verify(AuthObject creds) async {
72 | final v = await validate(creds.url, creds.device);
73 | if (v.status == 0) {
74 | state = AuthState.error;
75 | return false;
76 | }
77 | if (v.status == 404) {
78 | await clear(creds.device);
79 | login();
80 | return false;
81 | }
82 | return true;
83 | }
84 |
85 | Future delete(String id) async {
86 | logWarning(id);
87 | await db.users?.delete(id);
88 | check(showSelect: true);
89 | }
90 |
91 | Future clear(String device) async {
92 | await db.users?.delete(device);
93 | }
94 |
95 | Future validate(String url, String id) async {
96 | return (await validation.check(url, id)).match(
97 | (l) => ValidationStatus(0, null),
98 | (r) => ValidationStatus(r["status"], r["user"]));
99 | }
100 |
101 | Future getCredentials() async {
102 | final allcreds = await getAllCreds();
103 | final me = allcreds[0];
104 |
105 | return {"url": me.url, "device": me.device};
106 | }
107 |
108 | Future> getAllCreds() async {
109 | final List? allcreds = await db.users?.getAllKeys();
110 | if (allcreds == null || allcreds.isEmpty) {
111 | return [];
112 | }
113 |
114 | final creds = [];
115 | await Future.forEach(allcreds, (String c) async {
116 | final data = await db.users?.get(c);
117 | creds.add(AuthObject(c, data["apiUrl"], data["user"]));
118 | });
119 |
120 | return creds;
121 | }
122 |
123 | Future login() async {
124 | state = AuthState.login;
125 | ref.read(urlProvider.notifier).state = "";
126 | final code = ref.refresh(codeProvider);
127 | logInfo(code);
128 | bool result = false;
129 |
130 | final wsUrl = Uri.parse('wss://ntfy.sh/odinmovieshow-$code/ws');
131 | final channel = WebSocketChannel.connect(wsUrl);
132 |
133 | await channel.ready;
134 | var url = "";
135 | var id = "";
136 | final listen = channel.stream.listen((event) {
137 | try {
138 | var data = json.decode(event as String);
139 | if (data["message"] != null) {
140 | var m = json.decode(data["message"]);
141 | url = m["url"].replaceAll('"', "");
142 | id = m["deviceId"].replaceAll('"', "");
143 | result = true;
144 | }
145 | } catch (_) {
146 | // print(e);
147 | }
148 | });
149 |
150 | while (!result) {
151 | await Future.delayed(const Duration(seconds: 1));
152 | }
153 |
154 | logInfo(url);
155 | logInfo(id);
156 |
157 | listen.cancel();
158 |
159 | ref.read(urlProvider.notifier).state = url;
160 | await Future.delayed(const Duration(seconds: 5));
161 |
162 | final v = await validate(url, id);
163 |
164 | if (v.status > 0 && v.status < 300) {
165 | await db.users?.put(id, {"apiUrl": url, "user": v.user});
166 | logOk("Auth: Successful!");
167 | me = AuthObject(id, url, v.user);
168 | state = AuthState.ok;
169 | } else {
170 | if (v.status == 0) {
171 | ref.read(errorProvider.notifier).state =
172 | "Network error: Cannot reach URL";
173 | }
174 | if (v.status > 399) {
175 | ref.read(errorProvider.notifier).state =
176 | "Authorization error: Something is wrong";
177 | }
178 | state = AuthState.login;
179 | return await login();
180 | }
181 | }
182 | }
183 |
184 | final urlProvider = StateProvider((ref) {
185 | return "";
186 | });
187 |
188 | final usersProvider = FutureProvider>((ref) {
189 | return ref.watch(authProvider.notifier).getAllCreds();
190 | });
191 |
192 | final errorProvider = StateProvider((ref) {
193 | return "";
194 | });
195 |
196 | final authProvider = StateNotifierProvider((ref) => AuthModel(
197 | ref, ref.watch(dbProvider.notifier), ref.watch(validationProvider)));
198 |
199 | final codeProvider = StateProvider((ref) {
200 | String c = const UuidV4().generate().split("-").first.toString();
201 | return c;
202 | });
203 |
--------------------------------------------------------------------------------
/lib/data/models/item_model.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_riverpod/flutter_riverpod.dart';
2 | import 'package:helpers/helpers/print.dart';
3 | import 'package:odin/data/services/trakt_service.dart';
4 |
5 | import 'package:odin/data/entities/trakt.dart';
6 | import 'package:odin/helpers.dart';
7 |
8 | var genres = [
9 | {"name": "Action", "slug": "action"},
10 | {"name": "Adventure", "slug": "adventure"},
11 | {"name": "Animation", "slug": "animation"},
12 | {"name": "Anime", "slug": "anime"},
13 | {"name": "Comedy", "slug": "comedy"},
14 | {"name": "Crime", "slug": "crime"},
15 | {"name": "Documentary", "slug": "documentary"},
16 | {"name": "Drama", "slug": "drama"},
17 | {"name": "Family", "slug": "family"},
18 | {"name": "Fantasy", "slug": "fantasy"},
19 | {"name": "History", "slug": "history"},
20 | {"name": "Holiday", "slug": "holiday"},
21 | {"name": "Horror", "slug": "horror"},
22 | {"name": "Music", "slug": "music"},
23 | {"name": "Musical", "slug": "musical"},
24 | {"name": "Mystery", "slug": "mystery"},
25 | {"name": "None", "slug": "none"},
26 | {"name": "Romance", "slug": "romance"},
27 | {"name": "Science Fiction", "slug": "science-fiction"},
28 | {"name": "Short", "slug": "short"},
29 | {"name": "Sporting Event", "slug": "sporting-event"},
30 | {"name": "Superhero", "slug": "superhero"},
31 | {"name": "Suspense", "slug": "suspense"},
32 | {"name": "Thriller", "slug": "thriller"},
33 | {"name": "War", "slug": "war"},
34 | {"name": "Western", "slug": "western"}
35 | ];
36 |
37 | class ListData {
38 | int page = 1;
39 | List items = [];
40 | }
41 |
42 | class SectionItem {
43 | String title;
44 | String url;
45 | String? type;
46 | bool? isGenre;
47 | bool big;
48 | bool filterWatched;
49 | bool isTodayTomorrowEpisodes;
50 | List items = [];
51 | bool paginate;
52 |
53 | SectionItem({
54 | this.title = '',
55 | this.url = '',
56 | this.type,
57 | this.isGenre,
58 | this.big = false,
59 | this.filterWatched = false,
60 | this.isTodayTomorrowEpisodes = false,
61 | this.items = const [],
62 | this.paginate = true,
63 | });
64 | }
65 |
66 | class ListSetting {
67 | ListSetting(this.url, this.page);
68 | String url = '';
69 | int page = 1;
70 | }
71 |
72 | class ItemsProvider extends StateNotifier> {
73 | final Ref ref;
74 | final ItemsProviderData data;
75 | final TraktService traktService;
76 | final dynamic appRefreshProvider;
77 | int page = 0;
78 | // final page;
79 | // List items = [];
80 | ItemsProvider(this.ref, this.traktService, this.appRefreshProvider, this.data)
81 | : super([]) {
82 | page = 1;
83 | load();
84 | }
85 |
86 | String getUrl(String url) {
87 | var append = '?';
88 | if (url.contains('?')) {
89 | append = '&';
90 | }
91 | return '$url${append}limit=30&page=$page';
92 | }
93 |
94 | void load() async {
95 | Future.delayed(const Duration(milliseconds: 10), () {
96 | appRefreshProvider.state = true;
97 | });
98 | List list =
99 | await ref.watch(traktProvider).getItems(getUrl(data.url));
100 |
101 | if (data.filterWatched) {
102 | list = list.where((i) => i.watched == false).toList();
103 | }
104 |
105 | state = [...state, ...list];
106 | Future.delayed(const Duration(milliseconds: 10), () {
107 | appRefreshProvider.state = false;
108 | });
109 | }
110 |
111 | void next() async {
112 | page++;
113 | load();
114 | }
115 | }
116 |
117 | class ItemsProviderData {
118 | final String url;
119 | final bool filterWatched;
120 | ItemsProviderData(this.url, this.filterWatched);
121 | }
122 |
123 | final itemsProvider = AutoDisposeStateNotifierProviderFamily, ItemsProviderData>((ref, data) {
125 | return ItemsProvider(ref, ref.watch(traktProvider),
126 | ref.read(appRefreshProvider.notifier), data);
127 | });
128 |
129 | final appRefreshProvider = StateProvider.autoDispose((ref) => false);
130 |
131 | final genreProvider =
132 | StateProvider.autoDispose.family((ref, type) => 0);
133 |
--------------------------------------------------------------------------------
/lib/data/models/settings_model.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_riverpod/flutter_riverpod.dart';
2 | import 'package:odin/data/entities/config.dart';
3 | import 'package:odin/data/models/auth_model.dart';
4 | import 'package:odin/helpers.dart';
5 |
6 | import '../services/db.dart';
7 |
8 | List