You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
multimedia_flutter/dist/multimediaFlutter-src-20251...

1716 lines
54 KiB
Plaintext

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

===== 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