|
|
===== BEGIN FILE: analysis_options.yaml =====
|
|
|
# This file configures the analyzer, which statically analyzes Dart code to
|
|
|
# check for errors, warnings, and lints.
|
|
|
#
|
|
|
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
|
|
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
|
|
# invoked from the command line by running `flutter analyze`.
|
|
|
|
|
|
# The following line activates a set of recommended lints for Flutter apps,
|
|
|
# packages, and plugins designed to encourage good coding practices.
|
|
|
include: package:flutter_lints/flutter.yaml
|
|
|
|
|
|
linter:
|
|
|
# The lint rules applied to this project can be customized in the
|
|
|
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
|
|
# included above or to enable additional rules. A list of all available lints
|
|
|
# and their documentation is published at https://dart.dev/lints.
|
|
|
#
|
|
|
# Instead of disabling a lint rule for the entire project in the
|
|
|
# section below, it can also be suppressed for a single line of code
|
|
|
# or a specific dart file by using the `// ignore: name_of_lint` and
|
|
|
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
|
|
# producing the lint.
|
|
|
rules:
|
|
|
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
|
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
|
|
|
|
|
# Additional information about this file can be found at
|
|
|
# https://dart.dev/guides/language/analysis-options
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: pubspec.yaml =====
|
|
|
name: multimediaFlutter
|
|
|
description: Verwaltung von Videos & Serien mit Riverpod, TMDB und PHP-Backend
|
|
|
publish_to: 'none'
|
|
|
version: 0.1.0+1
|
|
|
|
|
|
environment:
|
|
|
sdk: ">=3.4.0 <4.0.0"
|
|
|
|
|
|
dependencies:
|
|
|
flutter:
|
|
|
sdk: flutter
|
|
|
flutter_riverpod: ^2.5.1
|
|
|
dio: ^5.7.0
|
|
|
cached_network_image: ^3.3.1
|
|
|
intl: ^0.19.0
|
|
|
|
|
|
# Material 3 ist im Flutter SDK enthalten
|
|
|
|
|
|
dev_dependencies:
|
|
|
flutter_test:
|
|
|
sdk: flutter
|
|
|
flutter_lints: ^5.0.0
|
|
|
|
|
|
flutter:
|
|
|
uses-material-design: true
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: README.md =====
|
|
|
# multimediaFlutter
|
|
|
|
|
|
A new Flutter project created with FlutLab - https://flutlab.io
|
|
|
|
|
|
## Getting Started
|
|
|
|
|
|
A few resources to get you started if this is your first Flutter project:
|
|
|
- https://flutter.dev/docs/get-started/codelab
|
|
|
- https://flutter.dev/docs/cookbook
|
|
|
|
|
|
For help getting started with Flutter, view https://flutter.dev/docs which offers tutorials, samples, guidance on mobile development, and a full API reference.
|
|
|
|
|
|
## Getting Started: FlutLab - Flutter Online IDE
|
|
|
- How to use FlutLab? Please, view https://flutlab.io/docs
|
|
|
- Join the discussion and conversation on https://flutlab.io/residents
|
|
|
|
|
|
## Upload to ChatGPT Project (ohne Git)
|
|
|
1. Optional: `flutter clean` im Repo ausführen.
|
|
|
2. Paket bauen: `pwsh tool/export.ps1` (oder `pwsh tool/export.ps1 -Clean`).
|
|
|
3. In ChatGPT Project "Multimedia in Flutter/Dart" → Add files → die erzeugte ZIP aus `dist/` hochladen.
|
|
|
4. Bei Änderungen Schritt 2 wiederholen und erneut hochladen.
|
|
|
|
|
|
Ausgeschlossene Verzeichnisse beim Export: `build/`, `.dart_tool/`, `ios/Pods/`, `android/build/`, `.idea/`, `.vscode/`, `web/build/`, `android/.gradle`, `dist/`, `tool/`, `.git/`, `.github/`.
|
|
|
|
|
|
## Windows Schnellstart
|
|
|
- Doppelklick: `tool/export.bat` erstellt ein ZIP unter `dist/`.
|
|
|
- Optional sauber bauen: `tool/export.bat -Clean`
|
|
|
- Danach im ChatGPT Project "Multimedia in Flutter/Dart" → Add files → ZIP hochladen.
|
|
|
|
|
|
Hinweis
|
|
|
- Eine Flutter-`.gitignore` ist enthalten und kann später für dein eigenes Git-Repo verwendet werden.
|
|
|
|
|
|
### Tipps bei "File content may not be accessible"
|
|
|
- Lade statt ZIP einen Ordner hoch: `pwsh tool/export.ps1 -AsFolder` und den erzeugten Ordner aus `dist/` per Drag & Drop ins Project ziehen.
|
|
|
- Minimalen Quellcode exportieren: `pwsh tool/export.ps1 -Minimal` (oder kombiniert: `-Minimal -AsFolder`).
|
|
|
- Große/binary Dateien (z. B. Bilder, .jar) können im Project nicht angezeigt werden – das ist normal. Für Code-Kontext reicht der Minimal-Export.
|
|
|
|
|
|
### Upload-Limit (10 Dateien)
|
|
|
- Ein-Datei-Bundle: `pwsh tool/export.ps1 -Minimal -Bundle` (empfohlen für ChatGPT Anzeige). Upload nur der erzeugten `*-bundle.txt`.
|
|
|
- Alternativ, in 10er-Pakete splitten: `pwsh tool/export.ps1 -Minimal -AsFolder -Chunk 10` und die erzeugten `pack-***` Ordner nacheinander hochladen.
|
|
|
- Standard-Ordner: `-AsFolder` ohne `-Chunk` erzeugt einen kompletten Ordner (ggf. in mehreren Uploads á 10 Dateien hochladen).
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: android\gradle.properties =====
|
|
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
|
|
android.useAndroidX=true
|
|
|
android.enableJetifier=true
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: android\gradlew.bat =====
|
|
|
@if "%DEBUG%" == "" @echo off
|
|
|
@rem ##########################################################################
|
|
|
@rem
|
|
|
@rem Gradle startup script for Windows
|
|
|
@rem
|
|
|
@rem ##########################################################################
|
|
|
|
|
|
@rem Set local scope for the variables with windows NT shell
|
|
|
if "%OS%"=="Windows_NT" setlocal
|
|
|
|
|
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
|
set DEFAULT_JVM_OPTS=
|
|
|
|
|
|
set DIRNAME=%~dp0
|
|
|
if "%DIRNAME%" == "" set DIRNAME=.
|
|
|
set APP_BASE_NAME=%~n0
|
|
|
set APP_HOME=%DIRNAME%
|
|
|
|
|
|
@rem Find java.exe
|
|
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
|
|
|
|
|
set JAVA_EXE=java.exe
|
|
|
%JAVA_EXE% -version >NUL 2>&1
|
|
|
if "%ERRORLEVEL%" == "0" goto init
|
|
|
|
|
|
echo.
|
|
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
|
|
echo.
|
|
|
echo Please set the JAVA_HOME variable in your environment to match the
|
|
|
echo location of your Java installation.
|
|
|
|
|
|
goto fail
|
|
|
|
|
|
:findJavaFromJavaHome
|
|
|
set JAVA_HOME=%JAVA_HOME:"=%
|
|
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
|
|
|
|
|
if exist "%JAVA_EXE%" goto init
|
|
|
|
|
|
echo.
|
|
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
|
|
echo.
|
|
|
echo Please set the JAVA_HOME variable in your environment to match the
|
|
|
echo location of your Java installation.
|
|
|
|
|
|
goto fail
|
|
|
|
|
|
:init
|
|
|
@rem Get command-line arguments, handling Windowz variants
|
|
|
|
|
|
if not "%OS%" == "Windows_NT" goto win9xME_args
|
|
|
if "%@eval[2+2]" == "4" goto 4NT_args
|
|
|
|
|
|
:win9xME_args
|
|
|
@rem Slurp the command line arguments.
|
|
|
set CMD_LINE_ARGS=
|
|
|
set _SKIP=2
|
|
|
|
|
|
:win9xME_args_slurp
|
|
|
if "x%~1" == "x" goto execute
|
|
|
|
|
|
set CMD_LINE_ARGS=%*
|
|
|
goto execute
|
|
|
|
|
|
:4NT_args
|
|
|
@rem Get arguments from the 4NT Shell from JP Software
|
|
|
set CMD_LINE_ARGS=%$
|
|
|
|
|
|
:execute
|
|
|
@rem Setup the command line
|
|
|
|
|
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
|
|
|
|
|
@rem Execute Gradle
|
|
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
|
|
|
|
|
:end
|
|
|
@rem End local scope for the variables with windows NT shell
|
|
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
|
|
|
|
|
:fail
|
|
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
|
|
rem the _cmd.exe /c_ return code!
|
|
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
|
|
exit /b 1
|
|
|
|
|
|
:mainEnd
|
|
|
if "%OS%"=="Windows_NT" endlocal
|
|
|
|
|
|
:omega
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: android\local.properties =====
|
|
|
sdk.dir=C:\\Users\\herwi\\AppData\\Local\\Android\\sdk
|
|
|
flutter.sdk=C:\\dev\\flutter
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: android\app\src\debug\AndroidManifest.xml =====
|
|
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
|
<!-- The INTERNET permission is required for development. Specifically,
|
|
|
the Flutter tool needs it to communicate with the running application
|
|
|
to allow setting breakpoints, to provide hot reload, etc.
|
|
|
-->
|
|
|
<uses-permission android:name="android.permission.INTERNET"/>
|
|
|
</manifest>
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: android\app\src\main\AndroidManifest.xml =====
|
|
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
|
<application
|
|
|
android:label="multimediaflutter"
|
|
|
android:name="${applicationName}"
|
|
|
android:icon="@mipmap/ic_launcher">
|
|
|
<activity
|
|
|
android:name=".MainActivity"
|
|
|
android:exported="true"
|
|
|
android:launchMode="singleTop"
|
|
|
android:taskAffinity=""
|
|
|
android:theme="@style/LaunchTheme"
|
|
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
|
|
android:hardwareAccelerated="true"
|
|
|
android:windowSoftInputMode="adjustResize">
|
|
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
|
|
the Android process has started. This theme is visible to the user
|
|
|
while the Flutter UI initializes. After that, this theme continues
|
|
|
to determine the Window background behind the Flutter UI. -->
|
|
|
<meta-data
|
|
|
android:name="io.flutter.embedding.android.NormalTheme"
|
|
|
android:resource="@style/NormalTheme"
|
|
|
/>
|
|
|
<intent-filter>
|
|
|
<action android:name="android.intent.action.MAIN"/>
|
|
|
<category android:name="android.intent.category.LAUNCHER"/>
|
|
|
</intent-filter>
|
|
|
</activity>
|
|
|
<!-- Don't delete the meta-data below.
|
|
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
|
|
<meta-data
|
|
|
android:name="flutterEmbedding"
|
|
|
android:value="2" />
|
|
|
</application>
|
|
|
<!-- Required to query activities that can process text, see:
|
|
|
https://developer.android.com/training/package-visibility and
|
|
|
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
|
|
|
|
|
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
|
|
<queries>
|
|
|
<intent>
|
|
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
|
|
<data android:mimeType="text/plain"/>
|
|
|
</intent>
|
|
|
</queries>
|
|
|
</manifest>
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: android\app\src\main\kotlin\com\example\multimediaflutter\MainActivity.kt =====
|
|
|
package com.example.multimediaflutter
|
|
|
|
|
|
import io.flutter.embedding.android.FlutterActivity
|
|
|
|
|
|
class MainActivity : FlutterActivity()
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: android\app\src\main\res\drawable\launch_background.xml =====
|
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
|
<!-- Modify this file to customize your launch splash screen -->
|
|
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
|
|
<item android:drawable="@android:color/white" />
|
|
|
|
|
|
<!-- You can insert your own image assets here -->
|
|
|
<!-- <item>
|
|
|
<bitmap
|
|
|
android:gravity="center"
|
|
|
android:src="@mipmap/launch_image" />
|
|
|
</item> -->
|
|
|
</layer-list>
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: android\app\src\main\res\drawable-v21\launch_background.xml =====
|
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
|
<!-- Modify this file to customize your launch splash screen -->
|
|
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
|
|
<item android:drawable="?android:colorBackground" />
|
|
|
|
|
|
<!-- You can insert your own image assets here -->
|
|
|
<!-- <item>
|
|
|
<bitmap
|
|
|
android:gravity="center"
|
|
|
android:src="@mipmap/launch_image" />
|
|
|
</item> -->
|
|
|
</layer-list>
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: android\app\src\main\res\values\styles.xml =====
|
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
|
<resources>
|
|
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
|
|
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
|
|
<!-- Show a splash screen on the activity. Automatically removed when
|
|
|
the Flutter engine draws its first frame -->
|
|
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
|
|
</style>
|
|
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
|
|
This theme determines the color of the Android Window while your
|
|
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
|
|
running.
|
|
|
|
|
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
|
|
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
|
|
<item name="android:windowBackground">?android:colorBackground</item>
|
|
|
</style>
|
|
|
</resources>
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: android\app\src\main\res\values-night\styles.xml =====
|
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
|
<resources>
|
|
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
|
|
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
|
|
<!-- Show a splash screen on the activity. Automatically removed when
|
|
|
the Flutter engine draws its first frame -->
|
|
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
|
|
</style>
|
|
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
|
|
This theme determines the color of the Android Window while your
|
|
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
|
|
running.
|
|
|
|
|
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
|
|
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
|
|
<item name="android:windowBackground">?android:colorBackground</item>
|
|
|
</style>
|
|
|
</resources>
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: android\app\src\profile\AndroidManifest.xml =====
|
|
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
|
<!-- The INTERNET permission is required for development. Specifically,
|
|
|
the Flutter tool needs it to communicate with the running application
|
|
|
to allow setting breakpoints, to provide hot reload, etc.
|
|
|
-->
|
|
|
<uses-permission android:name="android.permission.INTERNET"/>
|
|
|
</manifest>
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: android\gradle\wrapper\gradle-wrapper.properties =====
|
|
|
distributionBase=GRADLE_USER_HOME
|
|
|
distributionPath=wrapper/dists
|
|
|
zipStoreBase=GRADLE_USER_HOME
|
|
|
zipStorePath=wrapper/dists
|
|
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: ios\Flutter\AppFrameworkInfo.plist =====
|
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
|
<plist version="1.0">
|
|
|
<dict>
|
|
|
<key>CFBundleDevelopmentRegion</key>
|
|
|
<string>en</string>
|
|
|
<key>CFBundleExecutable</key>
|
|
|
<string>App</string>
|
|
|
<key>CFBundleIdentifier</key>
|
|
|
<string>io.flutter.flutter.app</string>
|
|
|
<key>CFBundleInfoDictionaryVersion</key>
|
|
|
<string>6.0</string>
|
|
|
<key>CFBundleName</key>
|
|
|
<string>App</string>
|
|
|
<key>CFBundlePackageType</key>
|
|
|
<string>FMWK</string>
|
|
|
<key>CFBundleShortVersionString</key>
|
|
|
<string>1.0</string>
|
|
|
<key>CFBundleSignature</key>
|
|
|
<string>????</string>
|
|
|
<key>CFBundleVersion</key>
|
|
|
<string>1.0</string>
|
|
|
<key>MinimumOSVersion</key>
|
|
|
<string>12.0</string>
|
|
|
</dict>
|
|
|
</plist>
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: ios\Flutter\flutter_export_environment.sh =====
|
|
|
#!/bin/sh
|
|
|
# This is a generated file; do not edit or check into version control.
|
|
|
export "FLUTTER_ROOT=/home/ubuntu/flutter"
|
|
|
export "FLUTTER_APPLICATION_PATH=/home/ubuntu/projects/hello_world"
|
|
|
export "COCOAPODS_PARALLEL_CODE_SIGN=true"
|
|
|
export "FLUTTER_TARGET=lib/main.dart"
|
|
|
export "FLUTTER_BUILD_DIR=build"
|
|
|
export "FLUTTER_BUILD_NAME=1.0.0"
|
|
|
export "FLUTTER_BUILD_NUMBER=1"
|
|
|
export "DART_OBFUSCATION=false"
|
|
|
export "TRACK_WIDGET_CREATION=true"
|
|
|
export "TREE_SHAKE_ICONS=false"
|
|
|
export "PACKAGE_CONFIG=.dart_tool/package_config.json"
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: ios\Runner\AppDelegate.swift =====
|
|
|
import Flutter
|
|
|
import UIKit
|
|
|
|
|
|
@main
|
|
|
@objc class AppDelegate: FlutterAppDelegate {
|
|
|
override func application(
|
|
|
_ application: UIApplication,
|
|
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
|
|
) -> Bool {
|
|
|
GeneratedPluginRegistrant.register(with: self)
|
|
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: ios\Runner\Info.plist =====
|
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
|
<plist version="1.0">
|
|
|
<dict>
|
|
|
<key>CFBundleDevelopmentRegion</key>
|
|
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
|
|
<key>CFBundleDisplayName</key>
|
|
|
<string>Hello World</string>
|
|
|
<key>CFBundleExecutable</key>
|
|
|
<string>$(EXECUTABLE_NAME)</string>
|
|
|
<key>CFBundleIdentifier</key>
|
|
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
|
<key>CFBundleInfoDictionaryVersion</key>
|
|
|
<string>6.0</string>
|
|
|
<key>CFBundleName</key>
|
|
|
<string>multimediaflutter</string>
|
|
|
<key>CFBundleDisplayName</key>
|
|
|
<string>multimediaflutter</string>
|
|
|
<key>CFBundlePackageType</key>
|
|
|
<string>APPL</string>
|
|
|
<key>CFBundleShortVersionString</key>
|
|
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
|
|
<key>CFBundleSignature</key>
|
|
|
<string>????</string>
|
|
|
<key>CFBundleVersion</key>
|
|
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
|
|
<key>LSRequiresIPhoneOS</key>
|
|
|
<true/>
|
|
|
<key>UILaunchStoryboardName</key>
|
|
|
<string>LaunchScreen</string>
|
|
|
<key>UIMainStoryboardFile</key>
|
|
|
<string>Main</string>
|
|
|
<key>UISupportedInterfaceOrientations</key>
|
|
|
<array>
|
|
|
<string>UIInterfaceOrientationPortrait</string>
|
|
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
|
</array>
|
|
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
|
|
<array>
|
|
|
<string>UIInterfaceOrientationPortrait</string>
|
|
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
|
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
|
</array>
|
|
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
|
|
<true/>
|
|
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
|
|
<true/>
|
|
|
</dict>
|
|
|
</plist>
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: ios\Runner\Assets.xcassets\AppIcon.appiconset\Contents.json =====
|
|
|
{
|
|
|
"images" : [
|
|
|
{
|
|
|
"size" : "20x20",
|
|
|
"idiom" : "iphone",
|
|
|
"filename" : "Icon-App-20x20@2x.png",
|
|
|
"scale" : "2x"
|
|
|
},
|
|
|
{
|
|
|
"size" : "20x20",
|
|
|
"idiom" : "iphone",
|
|
|
"filename" : "Icon-App-20x20@3x.png",
|
|
|
"scale" : "3x"
|
|
|
},
|
|
|
{
|
|
|
"size" : "29x29",
|
|
|
"idiom" : "iphone",
|
|
|
"filename" : "Icon-App-29x29@1x.png",
|
|
|
"scale" : "1x"
|
|
|
},
|
|
|
{
|
|
|
"size" : "29x29",
|
|
|
"idiom" : "iphone",
|
|
|
"filename" : "Icon-App-29x29@2x.png",
|
|
|
"scale" : "2x"
|
|
|
},
|
|
|
{
|
|
|
"size" : "29x29",
|
|
|
"idiom" : "iphone",
|
|
|
"filename" : "Icon-App-29x29@3x.png",
|
|
|
"scale" : "3x"
|
|
|
},
|
|
|
{
|
|
|
"size" : "40x40",
|
|
|
"idiom" : "iphone",
|
|
|
"filename" : "Icon-App-40x40@2x.png",
|
|
|
"scale" : "2x"
|
|
|
},
|
|
|
{
|
|
|
"size" : "40x40",
|
|
|
"idiom" : "iphone",
|
|
|
"filename" : "Icon-App-40x40@3x.png",
|
|
|
"scale" : "3x"
|
|
|
},
|
|
|
{
|
|
|
"size" : "60x60",
|
|
|
"idiom" : "iphone",
|
|
|
"filename" : "Icon-App-60x60@2x.png",
|
|
|
"scale" : "2x"
|
|
|
},
|
|
|
{
|
|
|
"size" : "60x60",
|
|
|
"idiom" : "iphone",
|
|
|
"filename" : "Icon-App-60x60@3x.png",
|
|
|
"scale" : "3x"
|
|
|
},
|
|
|
{
|
|
|
"size" : "20x20",
|
|
|
"idiom" : "ipad",
|
|
|
"filename" : "Icon-App-20x20@1x.png",
|
|
|
"scale" : "1x"
|
|
|
},
|
|
|
{
|
|
|
"size" : "20x20",
|
|
|
"idiom" : "ipad",
|
|
|
"filename" : "Icon-App-20x20@2x.png",
|
|
|
"scale" : "2x"
|
|
|
},
|
|
|
{
|
|
|
"size" : "29x29",
|
|
|
"idiom" : "ipad",
|
|
|
"filename" : "Icon-App-29x29@1x.png",
|
|
|
"scale" : "1x"
|
|
|
},
|
|
|
{
|
|
|
"size" : "29x29",
|
|
|
"idiom" : "ipad",
|
|
|
"filename" : "Icon-App-29x29@2x.png",
|
|
|
"scale" : "2x"
|
|
|
},
|
|
|
{
|
|
|
"size" : "40x40",
|
|
|
"idiom" : "ipad",
|
|
|
"filename" : "Icon-App-40x40@1x.png",
|
|
|
"scale" : "1x"
|
|
|
},
|
|
|
{
|
|
|
"size" : "40x40",
|
|
|
"idiom" : "ipad",
|
|
|
"filename" : "Icon-App-40x40@2x.png",
|
|
|
"scale" : "2x"
|
|
|
},
|
|
|
{
|
|
|
"size" : "76x76",
|
|
|
"idiom" : "ipad",
|
|
|
"filename" : "Icon-App-76x76@1x.png",
|
|
|
"scale" : "1x"
|
|
|
},
|
|
|
{
|
|
|
"size" : "76x76",
|
|
|
"idiom" : "ipad",
|
|
|
"filename" : "Icon-App-76x76@2x.png",
|
|
|
"scale" : "2x"
|
|
|
},
|
|
|
{
|
|
|
"size" : "83.5x83.5",
|
|
|
"idiom" : "ipad",
|
|
|
"filename" : "Icon-App-83.5x83.5@2x.png",
|
|
|
"scale" : "2x"
|
|
|
},
|
|
|
{
|
|
|
"size" : "1024x1024",
|
|
|
"idiom" : "ios-marketing",
|
|
|
"filename" : "Icon-App-1024x1024@1x.png",
|
|
|
"scale" : "1x"
|
|
|
}
|
|
|
],
|
|
|
"info" : {
|
|
|
"version" : 1,
|
|
|
"author" : "xcode"
|
|
|
}
|
|
|
}
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: ios\Runner\Assets.xcassets\LaunchImage.imageset\Contents.json =====
|
|
|
{
|
|
|
"images" : [
|
|
|
{
|
|
|
"idiom" : "universal",
|
|
|
"filename" : "LaunchImage.png",
|
|
|
"scale" : "1x"
|
|
|
},
|
|
|
{
|
|
|
"idiom" : "universal",
|
|
|
"filename" : "LaunchImage@2x.png",
|
|
|
"scale" : "2x"
|
|
|
},
|
|
|
{
|
|
|
"idiom" : "universal",
|
|
|
"filename" : "LaunchImage@3x.png",
|
|
|
"scale" : "3x"
|
|
|
}
|
|
|
],
|
|
|
"info" : {
|
|
|
"version" : 1,
|
|
|
"author" : "xcode"
|
|
|
}
|
|
|
}
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: ios\Runner\Assets.xcassets\LaunchImage.imageset\README.md =====
|
|
|
# Launch Screen Assets
|
|
|
|
|
|
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
|
|
|
|
|
|
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: ios\Runner.xcodeproj\project.xcworkspace\xcshareddata\IDEWorkspaceChecks.plist =====
|
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
|
<plist version="1.0">
|
|
|
<dict>
|
|
|
<key>IDEDidComputeMac32BitWarning</key>
|
|
|
<true/>
|
|
|
</dict>
|
|
|
</plist>
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: ios\Runner.xcworkspace\xcshareddata\IDEWorkspaceChecks.plist =====
|
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
|
<plist version="1.0">
|
|
|
<dict>
|
|
|
<key>IDEDidComputeMac32BitWarning</key>
|
|
|
<true/>
|
|
|
</dict>
|
|
|
</plist>
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: ios\RunnerTests\RunnerTests.swift =====
|
|
|
import Flutter
|
|
|
import UIKit
|
|
|
import XCTest
|
|
|
|
|
|
class RunnerTests: XCTestCase {
|
|
|
|
|
|
func testExample() {
|
|
|
// If you add code to the Runner application, consider adding tests here.
|
|
|
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: lib\app.dart =====
|
|
|
import 'package:flutter/material.dart';
|
|
|
import 'features/movies/presentation/movie_list_screen.dart';
|
|
|
import 'features/series/presentation/series_list_screen.dart';
|
|
|
import 'features/import/import_screen.dart';
|
|
|
import 'features/ping/ping_test_screen.dart';
|
|
|
|
|
|
class MultimediaApp extends StatelessWidget {
|
|
|
const MultimediaApp({super.key});
|
|
|
|
|
|
@override
|
|
|
Widget build(BuildContext context) {
|
|
|
return MaterialApp(
|
|
|
title: 'multimediaFlutter',
|
|
|
theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.blueGrey),
|
|
|
home: const _HomeTabs(),
|
|
|
debugShowCheckedModeBanner: false,
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
class _HomeTabs extends StatefulWidget {
|
|
|
const _HomeTabs();
|
|
|
|
|
|
@override
|
|
|
State<_HomeTabs> createState() => _HomeTabsState();
|
|
|
}
|
|
|
|
|
|
class _HomeTabsState extends State<_HomeTabs>
|
|
|
with SingleTickerProviderStateMixin {
|
|
|
late final TabController _controller;
|
|
|
|
|
|
@override
|
|
|
void initState() {
|
|
|
super.initState();
|
|
|
_controller = TabController(length: 4, vsync: this);
|
|
|
}
|
|
|
|
|
|
@override
|
|
|
void dispose() {
|
|
|
_controller.dispose();
|
|
|
super.dispose();
|
|
|
}
|
|
|
|
|
|
@override
|
|
|
Widget build(BuildContext context) {
|
|
|
return Scaffold(
|
|
|
appBar: AppBar(
|
|
|
title: const Text('multimediaFlutter'),
|
|
|
bottom: TabBar(
|
|
|
controller: _controller,
|
|
|
tabs: const [
|
|
|
Tab(text: 'Filme'),
|
|
|
Tab(text: 'Serien'),
|
|
|
Tab(text: 'Import'),
|
|
|
Tab(text: 'Ping'),
|
|
|
],
|
|
|
),
|
|
|
),
|
|
|
body: TabBarView(
|
|
|
controller: _controller,
|
|
|
children: const [
|
|
|
MovieListScreen(),
|
|
|
SeriesListScreen(),
|
|
|
ImportScreen(),
|
|
|
PingTestScreen(),
|
|
|
],
|
|
|
),
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: lib\main.dart =====
|
|
|
import 'package:flutter/material.dart';
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
import 'app.dart';
|
|
|
|
|
|
void main() {
|
|
|
WidgetsFlutterBinding.ensureInitialized();
|
|
|
runApp(const ProviderScope(child: MultimediaApp()));
|
|
|
}
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: lib\core\config.dart =====
|
|
|
/// Globale Konfiguration. Standard: von `--dart-define` lesen.
|
|
|
/// Fallbacks sind hilfreich für lokale Tests.
|
|
|
|
|
|
class AppConfig {
|
|
|
static const backendBaseUrl = String.fromEnvironment(
|
|
|
'BACKEND_BASE_URL',
|
|
|
defaultValue: 'https://api.windesign.at/multimedia.php',
|
|
|
);
|
|
|
|
|
|
/// Token eines vorhandenen Users in deiner DB (users.api_token)
|
|
|
static const backendToken = String.fromEnvironment(
|
|
|
'BACKEND_TOKEN',
|
|
|
defaultValue: 'dasistwiedereinverystrongtoken',
|
|
|
);
|
|
|
|
|
|
/// TMDB API Key (nur lesend). Für Public-Apps besser: Server-Proxy/Caching.
|
|
|
static const tmdbApiKey = String.fromEnvironment(
|
|
|
'TMDB_API_KEY',
|
|
|
|
|
|
defaultValue: 'a33271b9e54cdcb9a80680eaf5522f1b',
|
|
|
|
|
|
///defaultValue: 'eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJhMzMyNzFiOWU1NGNkY2I5YTgwNjgwZWFmNTUyMmYxYiIsIm5iZiI6MTM0ODc2NTY2MS4wLCJzdWIiOiI1MDY0ODdkZDE5YzI5NTY2M2MwMDBhOGIiLCJzY29wZXMiOlsiYXBpX3JlYWQiXSwidmVyc2lvbiI6MX0.m26QybYBGQVY8OuL87FFae3ThPqAnOqEwgbLMtnH0wo'
|
|
|
);
|
|
|
}
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: lib\core\status.dart =====
|
|
|
enum ItemStatus { Init, Progress, Done }
|
|
|
|
|
|
ItemStatus statusFromString(String? s) {
|
|
|
return ItemStatus.values.firstWhere(
|
|
|
(e) => e.name == (s ?? 'Init'),
|
|
|
orElse: () => ItemStatus.Init,
|
|
|
);
|
|
|
}
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: lib\core\api\backend_api.dart =====
|
|
|
import 'package:dio/dio.dart';
|
|
|
import '../../core/config.dart';
|
|
|
|
|
|
class BackendApi {
|
|
|
final Dio _dio;
|
|
|
|
|
|
BackendApi()
|
|
|
: _dio = Dio(
|
|
|
BaseOptions(
|
|
|
baseUrl: AppConfig.backendBaseUrl,
|
|
|
headers: const {
|
|
|
'Accept': 'application/json',
|
|
|
},
|
|
|
contentType: Headers.formUrlEncodedContentType,
|
|
|
validateStatus: (s) => s != null && s < 500,
|
|
|
),
|
|
|
);
|
|
|
|
|
|
Future<Map<String, dynamic>> _post(Map<String, dynamic> body) async {
|
|
|
final payload = {
|
|
|
'api_token': AppConfig.backendToken,
|
|
|
...body,
|
|
|
};
|
|
|
Map<String, dynamic>? map;
|
|
|
Response res;
|
|
|
try {
|
|
|
res = await _dio.post('', data: payload);
|
|
|
} catch (e) {
|
|
|
// Basic diagnostics
|
|
|
// ignore: avoid_print
|
|
|
print('Backend request failed. URL: ${_dio.options.baseUrl}, payload: $payload, error: $e');
|
|
|
throw Exception('Backend request failed: $e');
|
|
|
}
|
|
|
if (res.data is Map) {
|
|
|
map = res.data as Map<String, dynamic>;
|
|
|
}
|
|
|
if (res.statusCode != 200) {
|
|
|
// ignore: avoid_print
|
|
|
print('Backend HTTP ${res.statusCode}. URL: ${_dio.options.baseUrl}, payload: $payload, data: ${res.data}');
|
|
|
if (map != null && map.containsKey('error')) {
|
|
|
throw Exception('Backend HTTP ${res.statusCode}: ${map['error']}');
|
|
|
}
|
|
|
throw Exception('Backend HTTP ${res.statusCode}: ${res.data}');
|
|
|
}
|
|
|
if (map == null) {
|
|
|
throw Exception('Backend: Unexpected response format');
|
|
|
}
|
|
|
if (map['ok'] != true) {
|
|
|
// ignore: avoid_print
|
|
|
print('Backend logical error. URL: ${_dio.options.baseUrl}, payload: $payload, data: $map');
|
|
|
throw Exception('Backend responded with error: ${map['error']}');
|
|
|
}
|
|
|
return map;
|
|
|
}
|
|
|
|
|
|
Future<List<Map<String, dynamic>>> getMovies(
|
|
|
{String? status, String? q, int offset = 0, int limit = 50}) async {
|
|
|
final map = await _post({
|
|
|
'action': 'get_list',
|
|
|
'type': 'movie',
|
|
|
if (status != null) 'status': status,
|
|
|
if (q != null && q.isNotEmpty) 'q': q,
|
|
|
'offset': offset,
|
|
|
'limit': limit,
|
|
|
});
|
|
|
return (map['items'] as List).cast<Map<String, dynamic>>();
|
|
|
}
|
|
|
|
|
|
Future<void> setStatus({
|
|
|
required String type, // 'movie' | 'episode'
|
|
|
required int refId,
|
|
|
required String status, // 'Init' | 'Progress' | 'Done'
|
|
|
String? downloadPath,
|
|
|
}) async {
|
|
|
await _post({
|
|
|
'action': 'set_status',
|
|
|
'type': type,
|
|
|
'ref_id': refId,
|
|
|
'status': status,
|
|
|
if (downloadPath != null) 'download_path': downloadPath,
|
|
|
});
|
|
|
}
|
|
|
|
|
|
Future<void> upsertMovie(Map<String, dynamic> tmdbJson) async {
|
|
|
await _post({
|
|
|
'action': 'upsert_movie',
|
|
|
'tmdb': tmdbJson,
|
|
|
});
|
|
|
}
|
|
|
|
|
|
Future<List<Map<String, dynamic>>> getEpisodes({
|
|
|
String? status,
|
|
|
String? q,
|
|
|
int offset = 0,
|
|
|
int limit = 5000,
|
|
|
}) async {
|
|
|
final map = await _post({
|
|
|
'action': 'get_list',
|
|
|
'type': 'episode',
|
|
|
if (status != null) 'status': status,
|
|
|
if (q != null && q.isNotEmpty) 'q': q,
|
|
|
'offset': offset,
|
|
|
'limit': limit,
|
|
|
});
|
|
|
return (map['items'] as List).cast<Map<String, dynamic>>();
|
|
|
}
|
|
|
|
|
|
Future<void> upsertShow(Map<String, dynamic> tmdbJson) async {
|
|
|
await _post({'action': 'upsert_show', 'tmdb': tmdbJson});
|
|
|
}
|
|
|
|
|
|
Future<int> upsertSeason(int showId, Map<String, dynamic> seasonJson) async {
|
|
|
final map = await _post(
|
|
|
{'action': 'upsert_season', 'show_id': showId, 'tmdb': seasonJson});
|
|
|
return (map['id'] as num).toInt();
|
|
|
}
|
|
|
|
|
|
Future<int> upsertEpisode(
|
|
|
int seasonId, Map<String, dynamic> episodeJson) async {
|
|
|
final map = await _post({
|
|
|
'action': 'upsert_episode',
|
|
|
'season_id': seasonId,
|
|
|
'tmdb': episodeJson
|
|
|
});
|
|
|
return (map['id'] as num).toInt();
|
|
|
}
|
|
|
|
|
|
Future<int?> getShowDbIdByTmdbId(int tmdbId) async {
|
|
|
final map = await _post({'action': 'get_show_by_tmdb', 'tmdb_id': tmdbId});
|
|
|
final v = map['id'];
|
|
|
return v == null ? null : (v as num).toInt();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: lib\core\api\tmdb_api.dart =====
|
|
|
// lib/core/api/tmdb_api.dart
|
|
|
import 'package:dio/dio.dart';
|
|
|
import '../config.dart';
|
|
|
|
|
|
class TmdbApi {
|
|
|
final Dio _dio;
|
|
|
|
|
|
TmdbApi()
|
|
|
: _dio = Dio(
|
|
|
BaseOptions(
|
|
|
baseUrl: 'https://api.themoviedb.org/3',
|
|
|
headers: {
|
|
|
'Accept': 'application/json',
|
|
|
},
|
|
|
validateStatus: (code) => code != null && code < 500,
|
|
|
),
|
|
|
);
|
|
|
|
|
|
Map<String, dynamic> get _auth => {
|
|
|
'api_key': AppConfig.tmdbApiKey,
|
|
|
'language': 'de-DE',
|
|
|
};
|
|
|
|
|
|
Future<Map<String, dynamic>> getMovie(int id) async {
|
|
|
final res = await _dio.get(
|
|
|
'/movie/$id',
|
|
|
queryParameters: {
|
|
|
..._auth,
|
|
|
'append_to_response': 'images,credits',
|
|
|
},
|
|
|
);
|
|
|
if (res.statusCode != 200) {
|
|
|
throw Exception(
|
|
|
'TMDB getMovie($id) failed: ${res.statusCode} ${res.data}');
|
|
|
}
|
|
|
return Map<String, dynamic>.from(res.data);
|
|
|
}
|
|
|
|
|
|
Future<Map<String, dynamic>> getShow(int id) async {
|
|
|
final res = await _dio.get(
|
|
|
'/tv/$id',
|
|
|
queryParameters: {
|
|
|
..._auth,
|
|
|
'append_to_response': 'images,credits',
|
|
|
},
|
|
|
);
|
|
|
if (res.statusCode != 200) {
|
|
|
throw Exception(
|
|
|
'TMDB getShow($id) failed: ${res.statusCode} ${res.data}');
|
|
|
}
|
|
|
return Map<String, dynamic>.from(res.data);
|
|
|
}
|
|
|
|
|
|
Future<Map<String, dynamic>> getSeason(int showId, int seasonNumber) async {
|
|
|
final res = await _dio.get(
|
|
|
'/tv/$showId/season/$seasonNumber',
|
|
|
queryParameters: _auth,
|
|
|
);
|
|
|
if (res.statusCode != 200) {
|
|
|
throw Exception(
|
|
|
'TMDB getSeason($showId,S$seasonNumber) failed: ${res.statusCode} ${res.data}');
|
|
|
}
|
|
|
return Map<String, dynamic>.from(res.data);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: lib\features\import\import_screen.dart =====
|
|
|
import 'package:flutter/material.dart';
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
import 'package:dio/dio.dart';
|
|
|
import '../../core/api/tmdb_api.dart';
|
|
|
import '../../core/api/backend_api.dart';
|
|
|
import '../shared/providers.dart';
|
|
|
|
|
|
class ImportScreen extends ConsumerStatefulWidget {
|
|
|
const ImportScreen({super.key});
|
|
|
|
|
|
@override
|
|
|
ConsumerState<ImportScreen> createState() => _ImportScreenState();
|
|
|
}
|
|
|
|
|
|
class _ImportScreenState extends ConsumerState<ImportScreen> {
|
|
|
final _movieCtrl =
|
|
|
TextEditingController(text: '603, 27205'); // Matrix, Inception
|
|
|
final _showCtrl =
|
|
|
TextEditingController(text: '1396, 1399'); // Breaking Bad, GoT
|
|
|
String _log = '';
|
|
|
bool _busy = false;
|
|
|
|
|
|
void _append(String msg) => setState(() => _log += msg + '\n');
|
|
|
|
|
|
Future<void> _importMovies() async {
|
|
|
setState(() => _busy = true);
|
|
|
final tmdb = ref.read(tmdbApiProvider);
|
|
|
final backend = ref.read(backendApiProvider);
|
|
|
|
|
|
final ids = _movieCtrl.text
|
|
|
.split(RegExp(r'[,\s]+'))
|
|
|
.where((s) => s.isNotEmpty)
|
|
|
.map(int.parse);
|
|
|
for (final id in ids) {
|
|
|
try {
|
|
|
_append('Film $id: TMDB laden …');
|
|
|
final json = await tmdb.getMovie(id);
|
|
|
await backend.upsertMovie(json);
|
|
|
_append('Film $id: OK ✓');
|
|
|
} catch (e) {
|
|
|
_append('Film $id: Fehler → $e');
|
|
|
}
|
|
|
}
|
|
|
setState(() => _busy = false);
|
|
|
}
|
|
|
|
|
|
Future<void> _importShows() async {
|
|
|
setState(() => _busy = true);
|
|
|
final tmdb = ref.read(tmdbApiProvider);
|
|
|
final backend = ref.read(backendApiProvider);
|
|
|
|
|
|
final ids = _showCtrl.text
|
|
|
.split(RegExp(r'[,\s]+'))
|
|
|
.where((s) => s.isNotEmpty)
|
|
|
.map(int.parse);
|
|
|
|
|
|
for (final showId in ids) {
|
|
|
try {
|
|
|
_append('Serie $showId: TMDB laden …');
|
|
|
final showJson = await tmdb.getShow(showId);
|
|
|
print('SHOW JSON: $showJson'); // Debug-Ausgabe
|
|
|
|
|
|
await backend.upsertShow(showJson);
|
|
|
_append('Serie $showId: Show OK ✓');
|
|
|
|
|
|
final seasons = (showJson['seasons'] as List? ?? const [])
|
|
|
.where((s) => (s['season_number'] ?? 0) is int)
|
|
|
.cast<Map<String, dynamic>>();
|
|
|
|
|
|
for (final s in seasons) {
|
|
|
final seasonNo = (s['season_number'] as num).toInt();
|
|
|
if (seasonNo < 0) continue;
|
|
|
_append(' S$seasonNo: TMDB Season laden …');
|
|
|
|
|
|
final seasonJson = await tmdb.getSeason(showId, seasonNo);
|
|
|
final dbShowId = await _getDbShowIdByTmdb(backend, showId);
|
|
|
final dbSeasonId = await backend.upsertSeason(dbShowId, seasonJson);
|
|
|
|
|
|
_append(' S$seasonNo: Season OK (db:$dbSeasonId)');
|
|
|
|
|
|
final eps = (seasonJson['episodes'] as List? ?? const [])
|
|
|
.cast<Map<String, dynamic>>();
|
|
|
for (final e in eps) {
|
|
|
await backend.upsertEpisode(dbSeasonId, e);
|
|
|
}
|
|
|
_append(' S$seasonNo: ${eps.length} Episoden OK ✓');
|
|
|
}
|
|
|
} catch (e) {
|
|
|
// 👇 Hier kommt der erweiterte Catch hin!
|
|
|
if (e is DioException) {
|
|
|
print('❗ TMDB DioException für $showId');
|
|
|
print('➡️ Request: ${e.requestOptions.uri}');
|
|
|
print('➡️ Response: ${e.response?.data}');
|
|
|
print('➡️ Status: ${e.response?.statusCode}');
|
|
|
}
|
|
|
_append('Serie $showId: Fehler → $e');
|
|
|
}
|
|
|
}
|
|
|
setState(() => _busy = false);
|
|
|
}
|
|
|
|
|
|
Future<int> _getDbShowIdByTmdb(BackendApi backend, int tmdbId) async {
|
|
|
final id = await backend.getShowDbIdByTmdbId(tmdbId);
|
|
|
if (id == null) {
|
|
|
throw Exception(
|
|
|
'Show mit tmdb_id=$tmdbId nicht gefunden – zuerst upsert_show aufrufen.');
|
|
|
}
|
|
|
return id;
|
|
|
}
|
|
|
|
|
|
@override
|
|
|
Widget build(BuildContext context) {
|
|
|
final inputStyle = const TextStyle(fontSize: 13);
|
|
|
|
|
|
return Scaffold(
|
|
|
appBar: AppBar(title: const Text('Import (TMDB → DB)')),
|
|
|
body: Padding(
|
|
|
padding: const EdgeInsets.all(12),
|
|
|
child: Column(
|
|
|
children: [
|
|
|
Row(
|
|
|
children: [
|
|
|
const Text('Filme TMDB-IDs: '),
|
|
|
const SizedBox(width: 8),
|
|
|
Expanded(
|
|
|
child:
|
|
|
TextField(controller: _movieCtrl, style: inputStyle)),
|
|
|
const SizedBox(width: 8),
|
|
|
FilledButton(
|
|
|
onPressed: _busy ? null : _importMovies,
|
|
|
child: const Text('Import Filme'),
|
|
|
),
|
|
|
],
|
|
|
),
|
|
|
const SizedBox(height: 12),
|
|
|
Row(
|
|
|
children: [
|
|
|
const Text('Serien TMDB-IDs: '),
|
|
|
const SizedBox(width: 8),
|
|
|
Expanded(
|
|
|
child: TextField(controller: _showCtrl, style: inputStyle)),
|
|
|
const SizedBox(width: 8),
|
|
|
FilledButton(
|
|
|
onPressed: _busy ? null : _importShows,
|
|
|
child: const Text('Import Serien'),
|
|
|
),
|
|
|
],
|
|
|
),
|
|
|
const SizedBox(height: 12),
|
|
|
if (_busy) const LinearProgressIndicator(),
|
|
|
const SizedBox(height: 12),
|
|
|
Expanded(
|
|
|
child: Container(
|
|
|
padding: const EdgeInsets.all(8),
|
|
|
decoration: BoxDecoration(
|
|
|
border: Border.all(color: Colors.black12),
|
|
|
borderRadius: BorderRadius.circular(8),
|
|
|
),
|
|
|
alignment: Alignment.topLeft,
|
|
|
child: SingleChildScrollView(
|
|
|
child: SelectableText(_log,
|
|
|
style: const TextStyle(
|
|
|
fontFamily: 'monospace', fontSize: 12)),
|
|
|
),
|
|
|
),
|
|
|
),
|
|
|
],
|
|
|
),
|
|
|
),
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: lib\features\movies\data\movie_model.dart =====
|
|
|
import '../../../core/status.dart';
|
|
|
|
|
|
class Movie {
|
|
|
final int id; // DB-ID
|
|
|
final int tmdbId;
|
|
|
final String title;
|
|
|
final int? releaseYear;
|
|
|
final String? posterPath;
|
|
|
final ItemStatus status;
|
|
|
final String? downloadPath;
|
|
|
|
|
|
Movie({
|
|
|
required this.id,
|
|
|
required this.tmdbId,
|
|
|
required this.title,
|
|
|
this.releaseYear,
|
|
|
this.posterPath,
|
|
|
this.status = ItemStatus.Init,
|
|
|
this.downloadPath,
|
|
|
});
|
|
|
|
|
|
factory Movie.fromJson(Map<String, dynamic> j) => Movie(
|
|
|
id: j['id'] as int,
|
|
|
tmdbId: j['tmdb_id'] as int,
|
|
|
title: j['title'] as String,
|
|
|
releaseYear: j['release_year'] as int?,
|
|
|
posterPath: j['poster_path'] as String?,
|
|
|
status: j['status'] != null ? statusFromString(j['status'] as String) : ItemStatus.Init,
|
|
|
downloadPath: j['download_path'] as String?,
|
|
|
);
|
|
|
}
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: lib\features\movies\data\movie_repository.dart =====
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
import '../../../core/status.dart';
|
|
|
import '../../shared/providers.dart';
|
|
|
import 'movie_model.dart';
|
|
|
|
|
|
final movieFilterProvider = StateProvider<ItemStatus?>((_) => null);
|
|
|
|
|
|
final moviesProvider = FutureProvider.autoDispose<List<Movie>>((ref) async {
|
|
|
final backend = ref.watch(backendApiProvider);
|
|
|
final st = ref.watch(movieFilterProvider);
|
|
|
final list = await backend.getMovies(status: st?.name);
|
|
|
return list.map(Movie.fromJson).toList();
|
|
|
});
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: lib\features\movies\presentation\movie_list_screen.dart =====
|
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
|
|
import 'package:flutter/material.dart';
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
import '../../../core/status.dart';
|
|
|
import '../data/movie_repository.dart';
|
|
|
import 'widgets/status_chip.dart';
|
|
|
|
|
|
class MovieListScreen extends ConsumerWidget {
|
|
|
const MovieListScreen({super.key});
|
|
|
|
|
|
@override
|
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
|
final filter = ref.watch(movieFilterProvider);
|
|
|
final moviesAsync = ref.watch(moviesProvider);
|
|
|
|
|
|
return Padding(
|
|
|
padding: const EdgeInsets.all(12.0),
|
|
|
child: Column(
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
children: [
|
|
|
StatusChip(
|
|
|
selected: filter,
|
|
|
onChanged: (f) => ref.read(movieFilterProvider.notifier).state = f,
|
|
|
),
|
|
|
const SizedBox(height: 12),
|
|
|
Expanded(
|
|
|
child: moviesAsync.when(
|
|
|
data: (items) => RefreshIndicator(
|
|
|
onRefresh: () async => ref.invalidate(moviesProvider),
|
|
|
child: ListView.separated(
|
|
|
itemCount: items.length,
|
|
|
separatorBuilder: (_, __) => const Divider(height: 1),
|
|
|
itemBuilder: (context, i) {
|
|
|
final m = items[i];
|
|
|
return ListTile(
|
|
|
leading: m.posterPath != null
|
|
|
? ClipRRect(
|
|
|
borderRadius: BorderRadius.circular(6),
|
|
|
child: CachedNetworkImage(
|
|
|
imageUrl:
|
|
|
'https://image.tmdb.org/t/p/w154${m.posterPath}',
|
|
|
width: 50,
|
|
|
height: 75,
|
|
|
fit: BoxFit.cover,
|
|
|
),
|
|
|
)
|
|
|
: const SizedBox(width: 50, height: 75),
|
|
|
title: Text(m.title),
|
|
|
subtitle: Text([
|
|
|
if (m.releaseYear != null) m.releaseYear.toString(),
|
|
|
'Status: ${m.status.name}',
|
|
|
].join(' · ')),
|
|
|
trailing: Icon(_statusIcon(m.status)),
|
|
|
onTap: () {
|
|
|
// TODO: Detailseite / Status ändern
|
|
|
},
|
|
|
);
|
|
|
},
|
|
|
),
|
|
|
),
|
|
|
loading: () => const Center(child: CircularProgressIndicator()),
|
|
|
error: (e, st) => Center(
|
|
|
child: Text('Fehler: ${e.toString()}'),
|
|
|
),
|
|
|
),
|
|
|
),
|
|
|
],
|
|
|
),
|
|
|
);
|
|
|
}
|
|
|
|
|
|
IconData _statusIcon(ItemStatus s) {
|
|
|
switch (s) {
|
|
|
case ItemStatus.Init:
|
|
|
return Icons.hourglass_empty;
|
|
|
case ItemStatus.Progress:
|
|
|
return Icons.downloading;
|
|
|
case ItemStatus.Done:
|
|
|
return Icons.check_circle;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: lib\features\movies\presentation\widgets\status_chip.dart =====
|
|
|
import 'package:flutter/material.dart';
|
|
|
import 'package:multimediaFlutter/core/status.dart';
|
|
|
|
|
|
class StatusChip extends StatelessWidget {
|
|
|
final ItemStatus? selected;
|
|
|
final ValueChanged<ItemStatus?> onChanged;
|
|
|
|
|
|
const StatusChip(
|
|
|
{super.key, required this.selected, required this.onChanged});
|
|
|
|
|
|
@override
|
|
|
Widget build(BuildContext context) {
|
|
|
return Wrap(
|
|
|
spacing: 8,
|
|
|
children: [
|
|
|
FilterChip(
|
|
|
label: const Text('Alle'),
|
|
|
selected: selected == null,
|
|
|
onSelected: (_) => onChanged(null),
|
|
|
),
|
|
|
for (final s in ItemStatus.values)
|
|
|
FilterChip(
|
|
|
label: Text(s.name),
|
|
|
selected: selected == s,
|
|
|
onSelected: (_) => onChanged(s),
|
|
|
),
|
|
|
],
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: lib\features\ping\ping_test_screen.dart =====
|
|
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
import 'package:dio/dio.dart';
|
|
|
import '../../core/config.dart';
|
|
|
|
|
|
class PingTestScreen extends StatefulWidget {
|
|
|
const PingTestScreen({super.key});
|
|
|
|
|
|
@override
|
|
|
State<PingTestScreen> createState() => _PingTestScreenState();
|
|
|
}
|
|
|
|
|
|
class _PingTestScreenState extends State<PingTestScreen> {
|
|
|
String? _result;
|
|
|
bool _loading = false;
|
|
|
|
|
|
Future<void> _pingServer() async {
|
|
|
setState(() {
|
|
|
_loading = true;
|
|
|
_result = null;
|
|
|
});
|
|
|
|
|
|
try {
|
|
|
final res = await Dio(BaseOptions(
|
|
|
baseUrl: AppConfig.backendBaseUrl,
|
|
|
headers: const {
|
|
|
'Accept': 'application/json',
|
|
|
},
|
|
|
contentType: Headers.formUrlEncodedContentType,
|
|
|
validateStatus: (s) => s != null && s < 500,
|
|
|
)).post('', data: {'api_token': AppConfig.backendToken, 'action': 'ping'});
|
|
|
|
|
|
setState(() {
|
|
|
_result = '✅ Antwort: ${res.data}';
|
|
|
});
|
|
|
} catch (e) {
|
|
|
setState(() {
|
|
|
_result = '❌ Fehler: $e';
|
|
|
});
|
|
|
} finally {
|
|
|
setState(() => _loading = false);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@override
|
|
|
Widget build(BuildContext context) {
|
|
|
return Scaffold(
|
|
|
appBar: AppBar(title: const Text('Ping-Test')),
|
|
|
body: Center(
|
|
|
child: Padding(
|
|
|
padding: const EdgeInsets.all(16),
|
|
|
child: Column(
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
children: [
|
|
|
ElevatedButton.icon(
|
|
|
icon: const Icon(Icons.wifi_tethering),
|
|
|
label: const Text('Ping-Server'),
|
|
|
onPressed: _loading ? null : _pingServer,
|
|
|
),
|
|
|
const SizedBox(height: 20),
|
|
|
if (_loading) const CircularProgressIndicator(),
|
|
|
if (_result != null)
|
|
|
SelectableText(
|
|
|
_result!,
|
|
|
textAlign: TextAlign.center,
|
|
|
style: const TextStyle(fontSize: 14),
|
|
|
),
|
|
|
],
|
|
|
),
|
|
|
),
|
|
|
),
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: lib\features\series\data\episode_model.dart =====
|
|
|
import '../../../core/status.dart';
|
|
|
|
|
|
class EpisodeItem {
|
|
|
final int id;
|
|
|
final int episodeNumber;
|
|
|
final int seasonNumber;
|
|
|
final String showName;
|
|
|
final String? name;
|
|
|
final ItemStatus status;
|
|
|
|
|
|
EpisodeItem({
|
|
|
required this.id,
|
|
|
required this.episodeNumber,
|
|
|
required this.seasonNumber,
|
|
|
required this.showName,
|
|
|
this.name,
|
|
|
required this.status,
|
|
|
});
|
|
|
|
|
|
factory EpisodeItem.fromJson(Map<String, dynamic> j) => EpisodeItem(
|
|
|
id: j['id'] as int,
|
|
|
episodeNumber: j['episode_number'] as int,
|
|
|
seasonNumber: j['season_number'] as int,
|
|
|
showName: j['show_name'] as String,
|
|
|
name: j['name'] as String?,
|
|
|
status: statusFromString(j['status'] as String?),
|
|
|
);
|
|
|
}
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: lib\features\series\data\series_repository.dart =====
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
import '../../shared/providers.dart';
|
|
|
import '../../../core/status.dart';
|
|
|
import 'episode_model.dart';
|
|
|
|
|
|
final episodeFilterProvider = StateProvider<ItemStatus?>((_) => null);
|
|
|
|
|
|
final seriesGroupedProvider =
|
|
|
FutureProvider.autoDispose<SeriesGroupedData>((ref) async {
|
|
|
final backend = ref.watch(backendApiProvider);
|
|
|
final st = ref.watch(episodeFilterProvider);
|
|
|
final rows = await backend.getEpisodes(status: st?.name, limit: 5000);
|
|
|
final items = rows.map(EpisodeItem.fromJson).toList();
|
|
|
return SeriesGroupedData.fromEpisodes(items);
|
|
|
});
|
|
|
|
|
|
class SeriesGroupedData {
|
|
|
final Map<String, Map<int, List<EpisodeItem>>>
|
|
|
data; // showName -> season -> episodes
|
|
|
SeriesGroupedData(this.data);
|
|
|
|
|
|
factory SeriesGroupedData.fromEpisodes(List<EpisodeItem> items) {
|
|
|
final map = <String, Map<int, List<EpisodeItem>>>{};
|
|
|
for (final e in items) {
|
|
|
final bySeason =
|
|
|
map.putIfAbsent(e.showName, () => <int, List<EpisodeItem>>{});
|
|
|
final list = bySeason.putIfAbsent(e.seasonNumber, () => <EpisodeItem>[]);
|
|
|
list.add(e);
|
|
|
}
|
|
|
for (final bySeason in map.values) {
|
|
|
for (final eps in bySeason.values) {
|
|
|
eps.sort((a, b) => a.episodeNumber.compareTo(b.episodeNumber));
|
|
|
}
|
|
|
}
|
|
|
return SeriesGroupedData(map);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: lib\features\series\presentation\series_list_screen.dart =====
|
|
|
import 'package:flutter/material.dart';
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
import '../../../core/status.dart';
|
|
|
import '../../movies/presentation/widgets/status_chip.dart';
|
|
|
import '../data/series_repository.dart';
|
|
|
import 'widgets/episode_status_strip.dart';
|
|
|
|
|
|
class SeriesListScreen extends ConsumerWidget {
|
|
|
const SeriesListScreen({super.key});
|
|
|
|
|
|
@override
|
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
|
final filter = ref.watch(episodeFilterProvider);
|
|
|
final groupedAsync = ref.watch(seriesGroupedProvider);
|
|
|
|
|
|
return Scaffold(
|
|
|
body: Padding(
|
|
|
padding: const EdgeInsets.all(12),
|
|
|
child: Column(
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
children: [
|
|
|
StatusChip(
|
|
|
selected: filter,
|
|
|
onChanged: (f) =>
|
|
|
ref.read(episodeFilterProvider.notifier).state = f,
|
|
|
),
|
|
|
const SizedBox(height: 12),
|
|
|
Expanded(
|
|
|
child: groupedAsync.when(
|
|
|
loading: () => const Center(child: CircularProgressIndicator()),
|
|
|
error: (e, _) => Center(child: Text('Fehler: $e')),
|
|
|
data: (data) {
|
|
|
final shows = data.data.keys.toList()..sort();
|
|
|
if (shows.isEmpty) {
|
|
|
return const Center(
|
|
|
child: Text('Keine Episoden gefunden.'));
|
|
|
}
|
|
|
return ListView.builder(
|
|
|
itemCount: shows.length,
|
|
|
itemBuilder: (context, i) {
|
|
|
final showName = shows[i];
|
|
|
final seasons = data.data[showName]!;
|
|
|
final seasonNumbers = seasons.keys.toList()..sort();
|
|
|
return ExpansionTile(
|
|
|
title: Text(showName,
|
|
|
style:
|
|
|
const TextStyle(fontWeight: FontWeight.w600)),
|
|
|
children: [
|
|
|
for (final sNo in seasonNumbers)
|
|
|
Padding(
|
|
|
padding: const EdgeInsets.symmetric(
|
|
|
vertical: 8, horizontal: 12),
|
|
|
child: Row(
|
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
|
children: [
|
|
|
SizedBox(
|
|
|
width: 64,
|
|
|
child: Text('S$sNo',
|
|
|
style:
|
|
|
const TextStyle(fontFeatures: [])),
|
|
|
),
|
|
|
Expanded(
|
|
|
child: EpisodeStatusStrip(
|
|
|
episodes: seasons[sNo]!,
|
|
|
barWidth: 8,
|
|
|
barHeight: 28,
|
|
|
spacing: 3,
|
|
|
),
|
|
|
),
|
|
|
],
|
|
|
),
|
|
|
),
|
|
|
],
|
|
|
);
|
|
|
},
|
|
|
);
|
|
|
},
|
|
|
),
|
|
|
),
|
|
|
],
|
|
|
),
|
|
|
),
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: lib\features\series\presentation\widgets\episode_status_strip.dart =====
|
|
|
import 'package:flutter/material.dart';
|
|
|
import '../../../../core/status.dart';
|
|
|
import '../../data/episode_model.dart';
|
|
|
|
|
|
class EpisodeStatusStrip extends StatelessWidget {
|
|
|
final List<EpisodeItem> episodes;
|
|
|
final double barWidth; // Breite eines Balkens
|
|
|
final double barHeight; // Höhe eines Balkens
|
|
|
final double spacing; // Abstand zwischen Balken
|
|
|
|
|
|
const EpisodeStatusStrip({
|
|
|
super.key,
|
|
|
required this.episodes,
|
|
|
this.barWidth = 8,
|
|
|
this.barHeight = 28,
|
|
|
this.spacing = 3,
|
|
|
});
|
|
|
|
|
|
Color _fill(ItemStatus s) {
|
|
|
switch (s) {
|
|
|
case ItemStatus.Init:
|
|
|
return const Color(0xFFBDBDBD); // Grau
|
|
|
case ItemStatus.Progress:
|
|
|
return const Color(0xFF42A5F5); // Blau
|
|
|
case ItemStatus.Done:
|
|
|
return const Color(0xFF4CAF50); // Grün
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@override
|
|
|
Widget build(BuildContext context) {
|
|
|
if (episodes.isEmpty) return const SizedBox.shrink();
|
|
|
|
|
|
return SingleChildScrollView(
|
|
|
scrollDirection: Axis.horizontal,
|
|
|
child: Row(
|
|
|
children: [
|
|
|
for (int i = 0; i < episodes.length; i++) ...[
|
|
|
Container(
|
|
|
width: barWidth,
|
|
|
height: barHeight,
|
|
|
decoration: BoxDecoration(
|
|
|
color: _fill(episodes[i].status),
|
|
|
border: Border.all(color: Colors.black, width: 1), // „I“-Kontur
|
|
|
),
|
|
|
),
|
|
|
if (i != episodes.length - 1) SizedBox(width: spacing),
|
|
|
],
|
|
|
],
|
|
|
),
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: lib\features\shared\providers.dart =====
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
import '../../core/api/backend_api.dart';
|
|
|
import '../../core/api/tmdb_api.dart';
|
|
|
|
|
|
final backendApiProvider = Provider<BackendApi>((ref) => BackendApi());
|
|
|
final tmdbApiProvider = Provider<TmdbApi>((ref) => TmdbApi());
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: web\index.html =====
|
|
|
<!DOCTYPE html>
|
|
|
<html>
|
|
|
<head>
|
|
|
<!--
|
|
|
If you are serving your web app in a path other than the root, change the
|
|
|
href value below to reflect the base path you are serving from.
|
|
|
|
|
|
The path provided below has to start and end with a slash "/" in order for
|
|
|
it to work correctly.
|
|
|
|
|
|
For more details:
|
|
|
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
|
|
|
|
|
This is a placeholder for base href that will be replaced by the value of
|
|
|
the `--base-href` argument provided to `flutter build`.
|
|
|
-->
|
|
|
<base href="$FLUTTER_BASE_HREF">
|
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
|
|
<meta name="description" content="A new Flutter project.">
|
|
|
|
|
|
<!-- iOS meta tags & icons -->
|
|
|
<meta name="mobile-web-app-capable" content="yes">
|
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
|
|
<meta name="apple-mobile-web-app-title" content="hello_world">
|
|
|
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
|
|
|
|
|
<!-- Favicon -->
|
|
|
<link rel="icon" type="image/png" href="favicon.png"/>
|
|
|
|
|
|
<title>hello_world</title>
|
|
|
<link rel="manifest" href="manifest.json">
|
|
|
</head>
|
|
|
<body>
|
|
|
<script src="flutter_bootstrap.js" async></script>
|
|
|
</body>
|
|
|
</html>
|
|
|
|
|
|
\n===== END FILE =====\n
|
|
|
===== BEGIN FILE: web\manifest.json =====
|
|
|
{
|
|
|
"name": "hello_world",
|
|
|
"short_name": "hello_world",
|
|
|
"start_url": ".",
|
|
|
"display": "standalone",
|
|
|
"background_color": "#0175C2",
|
|
|
"theme_color": "#0175C2",
|
|
|
"description": "A new Flutter project.",
|
|
|
"orientation": "portrait-primary",
|
|
|
"prefer_related_applications": false,
|
|
|
"icons": [
|
|
|
{
|
|
|
"src": "icons/Icon-192.png",
|
|
|
"sizes": "192x192",
|
|
|
"type": "image/png"
|
|
|
},
|
|
|
{
|
|
|
"src": "icons/Icon-512.png",
|
|
|
"sizes": "512x512",
|
|
|
"type": "image/png"
|
|
|
},
|
|
|
{
|
|
|
"src": "icons/Icon-maskable-192.png",
|
|
|
"sizes": "192x192",
|
|
|
"type": "image/png",
|
|
|
"purpose": "maskable"
|
|
|
},
|
|
|
{
|
|
|
"src": "icons/Icon-maskable-512.png",
|
|
|
"sizes": "512x512",
|
|
|
"type": "image/png",
|
|
|
"purpose": "maskable"
|
|
|
}
|
|
|
]
|
|
|
}
|
|
|
|
|
|
\n===== END FILE =====\n
|