chore: init repository with project sources

main
Herwig Birke 2 months ago
commit 65f1175ba7

51
.gitignore vendored

@ -0,0 +1,51 @@
# General
.DS_Store
*.log
*.orig
# IDE
.vscode/
.idea/
*.iml
# Dart / Pub
.dart_tool/
.packages
.pub-cache/
.flutter-plugins
.flutter-plugins-dependencies
# Build / Coverage
build/
coverage/
# Android
android/.gradle/
android/local.properties
android/app/release/
android/app/profile/
android/app/debug/
# iOS
ios/Pods/
ios/.symlinks/
ios/Flutter/ephemeral/
ios/Flutter/Flutter.framework
ios/Flutter/App.framework
ios/Flutter/Flutter.podspec
ios/Flutter/Generated.xcconfig
ios/ServiceDefinitions.json
ios/Runner/GeneratedPluginRegistrant.*
ios/Runner.xcodeproj/project.xcworkspace/xcuserdata/
ios/Runner.xcworkspace/xcuserdata/
DerivedData/
# Web
web/build/
# Desktop
windows/runner/Debug/
windows/runner/Release/
macos/Flutter/ephemeral/
linux/flutter/ephemeral/

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

@ -0,0 +1,41 @@
# 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).

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

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

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

@ -0,0 +1,45 @@
<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>

@ -0,0 +1,29 @@
package io.flutter.plugins;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import io.flutter.Log;
import io.flutter.embedding.engine.FlutterEngine;
/**
* Generated file. Do not edit.
* This file is generated by the Flutter tool based on the
* plugins that support the Android platform.
*/
@Keep
public final class GeneratedPluginRegistrant {
private static final String TAG = "GeneratedPluginRegistrant";
public static void registerWith(@NonNull FlutterEngine flutterEngine) {
try {
flutterEngine.getPlugins().add(new io.flutter.plugins.pathprovider.PathProviderPlugin());
} catch (Exception e) {
Log.e(TAG, "Error registering plugin path_provider_android, io.flutter.plugins.pathprovider.PathProviderPlugin", e);
}
try {
flutterEngine.getPlugins().add(new com.tekartik.sqflite.SqflitePlugin());
} catch (Exception e) {
Log.e(TAG, "Error registering plugin sqflite_android, com.tekartik.sqflite.SqflitePlugin", e);
}
}
}

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

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

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

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

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

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

Binary file not shown.

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

160
android/gradlew vendored

@ -0,0 +1,160 @@
#!/usr/bin/env bash
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

@ -0,0 +1,90 @@
@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

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
</dict>
</plist>

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

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

@ -0,0 +1,13 @@
#!/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"

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

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

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

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

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

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

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

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

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

@ -0,0 +1,51 @@
<?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>

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

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

@ -0,0 +1,70 @@
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(),
],
),
);
}
}

@ -0,0 +1,182 @@
import 'dart:convert';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.jsonContentType,
validateStatus: (s) => s != null && s < 500,
),
);
Future<Map<String, dynamic>> _post(Map<String, dynamic> body) async {
Future<Response> sendJson(Map<String, dynamic> p) {
final prev = _dio.options.contentType;
_dio.options.contentType = Headers.jsonContentType;
return _dio.post('', data: p).whenComplete(() {
_dio.options.contentType = prev;
});
}
Future<Response> sendForm(Map<String, dynamic> p) {
final prev = _dio.options.contentType;
_dio.options.contentType = Headers.formUrlEncodedContentType;
final flat = p.map((k, v) => MapEntry(k, (v is Map || v is List) ? jsonEncode(v) : v));
return _dio.post('', data: flat).whenComplete(() {
_dio.options.contentType = prev;
});
}
Map<String, dynamic>? map;
Response? res;
final payload = {...body};
try {
res = await sendJson(payload);
} on DioException catch (e) {
// Capture 5xx with body
if (e.response != null) {
res = e.response;
} else {
// ignore: avoid_print
print('Backend request failed (JSON, no response). URL: ${_dio.options.baseUrl}, payload: $payload, error: $e');
throw Exception('Backend request failed: $e');
}
}
bool shouldFallbackToForm() {
final sc = res?.statusCode;
if (sc == 400) {
try {
final d = res?.data;
if (d is Map && (d['error']?.toString().toLowerCase().contains('unknown action') ?? false)) {
return true;
}
} catch (_) {}
}
return false;
}
if (shouldFallbackToForm()) {
// ignore: avoid_print
print('Retrying request as form-urlencoded due to unknown action (server likely expects \$_POST).');
try {
res = await sendForm(payload);
} on DioException catch (e) {
if (e.response != null) {
res = e.response;
} else {
// ignore: avoid_print
print('Backend request failed (FORM, no response). URL: ${_dio.options.baseUrl}, payload: $payload, error: $e');
throw Exception('Backend request failed: $e');
}
}
}
if (res != null && res.data is Map) {
map = res.data as Map<String, dynamic>;
}
if (res == null || res.statusCode != 200) {
// ignore: avoid_print
final sc = res?.statusCode;
final data = res?.data;
print('Backend HTTP ${sc}. URL: ${_dio.options.baseUrl}, payload: $payload, data: ${data}');
if (map != null && map.containsKey('error')) {
throw Exception('Backend HTTP ${sc}: ${map['error']}');
}
throw Exception('Backend HTTP ${sc}: ${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'
}) async {
await _post({
'action': 'set_status',
'type': type,
'ref_id': refId,
'status': status,
});
}
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();
}
}

@ -0,0 +1,65 @@
// 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);
}
}

@ -0,0 +1,24 @@
/// 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'
);
}

@ -0,0 +1,8 @@
enum ItemStatus { Init, Progress, Done }
ItemStatus statusFromString(String? s) {
return ItemStatus.values.firstWhere(
(e) => e.name == (s ?? 'Init'),
orElse: () => ItemStatus.Init,
);
}

@ -0,0 +1,172 @@
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)),
),
),
),
],
),
),
);
}
}

@ -0,0 +1,46 @@
import 'dart:convert';
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? resolution;
final String? overview;
Movie({
required this.id,
required this.tmdbId,
required this.title,
this.releaseYear,
this.posterPath,
this.status = ItemStatus.Init,
this.resolution,
this.overview,
});
factory Movie.fromJson(Map<String, dynamic> j) {
String? ov;
if (j['overview'] is String) {
ov = j['overview'] as String?;
} else if (j['json'] is String) {
try {
final m = jsonDecode(j['json'] as String);
if (m is Map && m['overview'] is String) ov = m['overview'] as String;
} catch (_) {}
}
return 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,
resolution: j['resolution'] as String?,
overview: ov,
);
}
}

@ -0,0 +1,13 @@
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();
});

@ -0,0 +1,167 @@
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) {
final itemsSorted = [...items]
..sort((a, b) => a.title.toLowerCase().compareTo(b.title.toLowerCase()));
return RefreshIndicator(
onRefresh: () async => ref.invalidate(moviesProvider),
child: ListView.separated(
itemCount: itemsSorted.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, i) {
final m = itemsSorted[i];
return ListTile(
tileColor: _statusBg(m.status, context),
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.releaseYear != null
? '${m.title} (${m.releaseYear})'
: m.title,
overflow: TextOverflow.ellipsis,
),
subtitle: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 56,
child: Align(
alignment: Alignment.topLeft,
child: FittedBox(
alignment: Alignment.topLeft,
fit: BoxFit.scaleDown,
child: _resolutionBadge(m.resolution, context),
),
),
),
const SizedBox(width: 6),
Expanded(
child: Text(
m.overview ?? '',
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
isThreeLine: true,
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;
}
}
Color? _statusBg(ItemStatus s, BuildContext context) {
switch (s) {
case ItemStatus.Init:
return Colors.grey.shade200;
case ItemStatus.Progress:
return Colors.blue;
case ItemStatus.Done:
return Colors.green;
}
}
Widget _resolutionBadge(String? res, BuildContext context) {
if (res == null || res.isEmpty) return const SizedBox.shrink();
final m = RegExp(r"\d+").firstMatch(res);
final v = m != null ? int.tryParse(m.group(0)!) : null;
IconData? icon;
Color color = Theme.of(context).colorScheme.primary;
String label = res;
if (v != null) {
if (v >= 2000) {
icon = Icons.four_k;
color = Colors.deepOrange;
label = '${v}p';
} else if (v >= 1000) {
icon = Icons.hd;
color = Colors.blue;
label = '${v}p';
} else {
icon = Icons.sd_card;
color = Colors.grey;
label = '${v}p';
}
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface.withOpacity(0.9),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: color.withOpacity(0.5)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) Icon(icon, size: 14, color: color),
if (icon != null) const SizedBox(width: 4),
Text(
label,
style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurface),
),
],
),
);
}
}

@ -0,0 +1,30 @@
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),
),
],
);
}
}

@ -0,0 +1,74 @@
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.jsonContentType,
validateStatus: (s) => s != null && s < 500,
)).post('', data: {'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),
),
],
),
),
),
);
}
}

@ -0,0 +1,57 @@
import '../../../core/status.dart';
class EpisodeItem {
final int id;
final int episodeNumber;
final int seasonNumber;
final String showName;
final String? name;
final ItemStatus status;
final String? resolution;
final int? firstAirYear;
final String? showJson;
final String? showStatus;
final bool? showCliffhanger;
final String? posterPath;
EpisodeItem({
required this.id,
required this.episodeNumber,
required this.seasonNumber,
required this.showName,
this.name,
required this.status,
this.resolution,
this.firstAirYear,
this.showJson,
this.showStatus,
this.showCliffhanger,
this.posterPath,
});
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?),
resolution: j['resolution'] as String?,
firstAirYear: j['first_air_year'] as int?,
showJson: j['show_json'] as String?,
showStatus: j['show_status'] as String?,
showCliffhanger: _parseBool(j['show_cliffhanger']),
posterPath: j['poster_path'] as String?,
);
static bool? _parseBool(dynamic v) {
if (v == null) return null;
if (v is bool) return v;
if (v is num) return v != 0;
if (v is String) {
final s = v.toLowerCase();
return s == '1' || s == 'true' || s == 'yes';
}
return null;
}
}

@ -0,0 +1,40 @@
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)
.where((e) => e.seasonNumber > 0)
.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);
}
}

@ -0,0 +1,235 @@
import 'dart:convert';
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 '../../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.'));
}
// Collect all season columns across all shows
final allSeasons = <int>{};
for (final m in data.data.values) {
allSeasons.addAll(m.keys);
}
final seasonCols = allSeasons.toList()..sort();
List<DataColumn> buildColumns() => [
const DataColumn(label: Text('Serie')),
for (final s in seasonCols) DataColumn(label: Text('Staffel $s')),
];
List<DataRow> buildRows() {
final rows = <DataRow>[];
for (final showName in shows) {
final bySeason = data.data[showName]!;
int? year;
String? resolution;
String? showJson;
String? showStatus;
bool? showCliffhanger;
String? posterPath;
final sortedSeasons = bySeason.keys.toList()..sort();
for (final s in sortedSeasons) {
final eps = bySeason[s]!;
for (final e in eps) {
year ??= e.firstAirYear;
resolution ??= e.resolution;
showJson ??= e.showJson;
showStatus ??= e.showStatus;
showCliffhanger ??= e.showCliffhanger;
posterPath ??= e.posterPath;
if (year != null && resolution != null && showStatus != null && showCliffhanger != null && posterPath != null) break;
}
if (year != null && resolution != null) break;
}
if ((year == null || showStatus == null || showCliffhanger == null) && showJson != null) {
try {
final m = jsonDecode(showJson!);
if (m is Map && m['first_air_date'] is String) {
final s = (m['first_air_date'] as String);
if (s.length >= 4) year = int.tryParse(s.substring(0, 4));
}
if (m is Map && showStatus == null && m['status'] is String) {
showStatus = m['status'] as String;
}
if (m is Map && showCliffhanger == null && m['cliffhanger'] != null) {
final v = m['cliffhanger'];
if (v is bool) showCliffhanger = v;
else if (v is num) showCliffhanger = v != 0;
else if (v is String) {
final s = v.toLowerCase();
showCliffhanger = (s == '1' || s == 'true' || s == 'yes');
}
}
} catch (_) {}
}
// Determine background for left cell based on episodes status
bool anyProgress = false;
bool anyInit = false;
for (final epsList in bySeason.values) {
for (final ep in epsList) {
if (ep.status == ItemStatus.Progress) anyProgress = true;
if (ep.status == ItemStatus.Init) anyInit = true;
}
}
Color? bg;
if (anyProgress) bg = Colors.blue;
else if (anyInit) bg = Colors.grey.shade200;
else bg = Colors.green;
final cells = <DataCell>[
DataCell(Container(
color: bg,
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
child: SizedBox(
width: 360,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (posterPath != null)
ClipRRect(
borderRadius: BorderRadius.circular(6),
child: CachedNetworkImage(
imageUrl: 'https://image.tmdb.org/t/p/w154$posterPath',
width: 50,
height: 75,
fit: BoxFit.cover,
),
),
if (posterPath != null) const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
_seriesTitle(showName, year, showStatus, showCliffhanger, context),
const SizedBox(height: 4),
_resolutionInline(resolution, context),
],
),
),
],
),
),
)),
];
for (final s in seasonCols) {
final eps = bySeason[s];
if (eps == null || eps.isEmpty) {
cells.add(const DataCell(Text('-', textAlign: TextAlign.center)));
} else {
cells.add(DataCell(EpisodeStatusStrip(
episodes: eps,
barWidth: 7,
barHeight: 25,
spacing: 0,
)));
}
}
rows.add(DataRow(cells: cells));
}
return rows;
}
final table = DataTable(
columns: buildColumns(),
rows: buildRows(),
headingRowHeight: 40,
dataRowMinHeight: 88,
dataRowMaxHeight: 96,
columnSpacing: 16,
);
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SingleChildScrollView(child: table),
);
},
),
),
],
),
),
);
}
Widget _resolutionInline(String? res, BuildContext context) {
if (res == null || res.isEmpty) return const SizedBox.shrink();
final m = RegExp(r"\d+").firstMatch(res);
final v = m != null ? int.tryParse(m.group(0)!) : null;
IconData? icon;
Color color = Theme.of(context).colorScheme.primary;
String label = res;
if (v != null) {
if (v >= 2000) {
icon = Icons.four_k;
color = Colors.deepOrange;
label = '${v}p';
} else if (v >= 1000) {
icon = Icons.hd;
color = Colors.blue;
label = '${v}p';
} else {
icon = Icons.sd_card;
color = Colors.grey;
label = '${v}p';
}
}
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) Icon(icon, size: 16, color: color),
if (icon != null) const SizedBox(width: 4),
Text(
label,
style: const TextStyle(fontSize: 12),
),
],
);
}
Widget _seriesTitle(String name, int? year, String? status, bool? cliff, BuildContext context) {
final base = Theme.of(context).textTheme.titleMedium;
final s = (status ?? '').toLowerCase();
final endedOrCanceled = s == 'ended' || s == 'canceled' || s == 'cancelled';
final fw = endedOrCanceled ? FontWeight.normal : FontWeight.w600;
final fs = (cliff == true) ? FontStyle.italic : FontStyle.normal;
final style = base?.copyWith(fontWeight: fw, fontStyle: fs);
final text = year != null ? '$name ($year)' : name;
return Text(text, overflow: TextOverflow.ellipsis, style: style);
}
}

@ -0,0 +1,59 @@
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 Quadrats
final double barHeight; // Höhe eines Quadrats
final double spacing; // Abstand zwischen Quadraten
const EpisodeStatusStrip({
super.key,
required this.episodes,
this.barWidth = 7,
this.barHeight = 25,
this.spacing = 0,
});
Color _fill(ItemStatus s) {
switch (s) {
case ItemStatus.Init:
return Colors.grey.shade200; // wie Filme (Init)
case ItemStatus.Progress:
return Colors.blue; // wie Filme (Progress)
case ItemStatus.Done:
return Colors.green; // wie Filme (Done)
}
}
@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(
left: BorderSide(color: Colors.black, width: i == 0 ? 1 : 0),
top: const BorderSide(color: Colors.black, width: 1),
right: const BorderSide(color: Colors.black, width: 1),
bottom: const BorderSide(color: Colors.black, width: 1),
),
),
),
if (i != episodes.length - 1 && spacing != 0) SizedBox(width: spacing),
],
],
),
);
}
}

@ -0,0 +1,6 @@
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());

@ -0,0 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'app.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const ProviderScope(child: MultimediaApp()));
}

@ -0,0 +1,27 @@
<?php
// multimedia-settings.php analog zu deinen bestehenden Projekten
const MM_ALLOWED_ORIGINS = [
'https://windesign.at',
'https://hedgehog.windesign.at',
'https://api.windesign.at',
'https://preview.flutlab.io',
'https://*.flutlab.io',
'http://localhost:*',
'http://127.0.0.1:*',
'https://localhost:*',
'https://127.0.0.1:*',
];
define('DATABASE_NAME', 'multimediaFlutter');
define('DATABASE_USER', 'multimediaFlutter');
define('DATABASE_PASSWORD', 'WeissIchNicht8');
define('DATABASE_HOST', 'localhost');
define('UPLOAD_DIR', __DIR__ . '/multimedia/uploads');
define('UPLOAD_THUMB_DIR', __DIR__ . '/multimedia/uploads/thumbs');
define('UPLOAD_BASE_URL', 'https://api.windesign.at/multimedia/uploads');
define('MAX_UPLOAD_SIZE', 512 * 1024 * 1024);
define('MM_DEBUG', true);
?>

@ -0,0 +1,365 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/multimedia-settings.php';
// --- CORS ---
function mm_origin_allowed(string $origin): bool {
foreach (MM_ALLOWED_ORIGINS as $pat) {
$rx = preg_quote($pat, '/');
$rx = str_replace(['\\*', '\\:\\*'], ['.*', '(?::\\d+)?'], $rx);
if (preg_match('/^'.$rx.'$/i', $origin)) return true;
}
return false;
}
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if ($origin && mm_origin_allowed($origin)) {
header('Access-Control-Allow-Origin: ' . $origin);
header('Vary: Origin');
} else {
header('Access-Control-Allow-Origin: https://windesign.at'); // Fallback
}
// WICHTIG: weit gefasste Allow-Headers (Browser senden oft zusätzliche)
$reqHeaders = $_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'] ?? '';
$allowHeaders = 'Content-Type, Accept, X-Requested-With, Authorization';
if ($reqHeaders) { $allowHeaders .= ', ' . $reqHeaders; }
header('Access-Control-Allow-Headers: ' . $allowHeaders);
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Max-Age: 86400'); // Preflights cachen
header('Access-Control-Allow-Credentials: false');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; }
function jsonBody(): array {
$raw = file_get_contents('php://input');
if (!$raw) return [];
$data = json_decode($raw, true);
return is_array($data) ? $data : [];
}
// Accept both JSON and form-urlencoded. Also decode JSON-like strings in keys we expect.
function inputBody(): array {
$in = jsonBody();
if (!$in) {
// Fallback to form POST
$in = $_POST ?: [];
}
// Normalize: decode JSON strings for nested payloads (e.g., tmdb)
foreach (['tmdb'] as $k) {
if (isset($in[$k]) && is_string($in[$k])) {
$d = json_decode($in[$k], true);
if (is_array($d)) $in[$k] = $d;
}
}
// Cast common numeric fields when present
foreach (['show_id','season_id','tmdb_id','ref_id','limit','offset'] as $k) {
if (isset($in[$k])) $in[$k] = (int)$in[$k];
}
return $in;
}
function resp($data, int $code = 200) { http_response_code($code); echo json_encode($data, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); exit; }
function fail($msg, int $code = 400) { resp(['ok'=>false,'error'=>$msg], $code); }
$dsn = sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4', DATABASE_HOST, DATABASE_NAME);
try {
$pdo = new PDO($dsn, DATABASE_USER, DATABASE_PASSWORD, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
} catch (Throwable $e) {
if (MM_DEBUG) fail('DB connection failed: '.$e->getMessage(), 500);
fail('DB connection failed', 500);
}
$in = inputBody();
$action = $in['action'] ?? null;
try {
switch ($action) {
case 'upsert_show': {
$tmdb = $in['tmdb'] ?? null; if (!$tmdb || !isset($tmdb['id'])) fail('missing tmdb payload');
try {
$stmt = $pdo->prepare("INSERT INTO shows (tmdb_id, name, original_name, first_air_year, poster_path, backdrop_path, json)
VALUES (?,?,?,?,?,?,?)
ON DUPLICATE KEY UPDATE name=VALUES(name), original_name=VALUES(original_name), first_air_year=VALUES(first_air_year), poster_path=VALUES(poster_path), backdrop_path=VALUES(backdrop_path), json=VALUES(json)");
$stmt->execute([
$tmdb['id'],
$tmdb['name'] ?? '',
$tmdb['original_name'] ?? null,
isset($tmdb['first_air_date']) ? intval(substr($tmdb['first_air_date'],0,4)) : null,
$tmdb['poster_path'] ?? null,
$tmdb['backdrop_path'] ?? null,
json_encode($tmdb, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),
]);
} catch (Throwable $e) {
// Fallback for schemas without original_name/first_air_year
if (strpos($e->getMessage(), 'Unknown column') !== false || ($e instanceof PDOException && $e->getCode()==='42S22')) {
$stmt = $pdo->prepare("INSERT INTO shows (tmdb_id, name, poster_path, backdrop_path, json)
VALUES (?,?,?,?,?)
ON DUPLICATE KEY UPDATE name=VALUES(name), poster_path=VALUES(poster_path), backdrop_path=VALUES(backdrop_path), json=VALUES(json)");
$stmt->execute([
$tmdb['id'],
$tmdb['name'] ?? '',
$tmdb['poster_path'] ?? null,
$tmdb['backdrop_path'] ?? null,
json_encode($tmdb, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),
]);
} else {
throw $e;
}
}
$id = $pdo->lastInsertId();
if (!$id) { $q=$pdo->prepare('SELECT id FROM shows WHERE tmdb_id=?'); $q->execute([$tmdb['id']]); $id=$q->fetchColumn(); }
resp(['ok'=>true,'id'=>(int)$id]);
}
case 'upsert_season': {
$showId = (int)($in['show_id'] ?? 0);
$tmdb = $in['tmdb'] ?? null; if (!$showId || !$tmdb) fail('bad params');
$seasonNo = isset($tmdb['season_number']) ? (int)$tmdb['season_number'] : null; if ($seasonNo===null) fail('missing season_number');
$name = $tmdb['name'] ?? (isset($seasonNo) ? ('Season '.$seasonNo) : '');
$airDate = $tmdb['air_date'] ?? null;
try {
$stmt = $pdo->prepare("INSERT INTO seasons (show_id, season_number, name, air_date, json)
VALUES (?,?,?,?,?)
ON DUPLICATE KEY UPDATE name=VALUES(name), air_date=VALUES(air_date), json=VALUES(json)");
$stmt->execute([$showId, $seasonNo, $name, $airDate, json_encode($tmdb, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES)]);
} catch (Throwable $e) {
if (strpos($e->getMessage(), 'Unknown column') !== false || ($e instanceof PDOException && $e->getCode()==='42S22')) {
$stmt = $pdo->prepare("INSERT INTO seasons (show_id, season_number, name, json)
VALUES (?,?,?,?)
ON DUPLICATE KEY UPDATE name=VALUES(name), json=VALUES(json)");
$stmt->execute([$showId, $seasonNo, $name, json_encode($tmdb, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES)]);
} else { throw $e; }
}
$id = $pdo->lastInsertId();
if (!$id) { $q=$pdo->prepare('SELECT id FROM seasons WHERE show_id=? AND season_number=?'); $q->execute([$showId,$seasonNo]); $id=$q->fetchColumn(); }
resp(['ok'=>true,'id'=>(int)$id]);
}
case 'upsert_episode': {
$seasonId = (int)($in['season_id'] ?? 0);
$tmdb = $in['tmdb'] ?? null; if (!$seasonId || !$tmdb) fail('bad params');
$epNo = isset($tmdb['episode_number']) ? (int)$tmdb['episode_number'] : null; if ($epNo===null) fail('missing episode_number');
$name = $tmdb['name'] ?? ('Episode '.$epNo);
$runtime = isset($tmdb['runtime']) ? (int)$tmdb['runtime'] : null;
$tmdbId = $tmdb['id'] ?? null;
try {
if ($tmdbId) {
$stmt = $pdo->prepare("INSERT INTO episodes (season_id, tmdb_id, episode_number, name, runtime, json)
VALUES (?,?,?,?,?,?)
ON DUPLICATE KEY UPDATE episode_number=VALUES(episode_number), name=VALUES(name), runtime=VALUES(runtime), json=VALUES(json)");
$stmt->execute([$seasonId, $tmdbId, $epNo, $name, $runtime, json_encode($tmdb, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES)]);
} else {
// No tmdb_id
$stmt = $pdo->prepare("INSERT INTO episodes (season_id, episode_number, name, runtime, json)
VALUES (?,?,?,?,?)
ON DUPLICATE KEY UPDATE name=VALUES(name), runtime=VALUES(runtime), json=VALUES(json)");
$stmt->execute([$seasonId, $epNo, $name, $runtime, json_encode($tmdb, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES)]);
}
} catch (Throwable $e) {
// Fallback if tmdb_id column doesn't exist: always use (season_id, episode_number)
if (strpos($e->getMessage(), 'Unknown column') !== false || ($e instanceof PDOException && $e->getCode()==='42S22')) {
$stmt = $pdo->prepare("INSERT INTO episodes (season_id, episode_number, name, runtime, json)
VALUES (?,?,?,?,?)
ON DUPLICATE KEY UPDATE name=VALUES(name), runtime=VALUES(runtime), json=VALUES(json)");
$stmt->execute([$seasonId, $epNo, $name, $runtime, json_encode($tmdb, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES)]);
} else { throw $e; }
}
$id = $pdo->lastInsertId();
if (!$id) {
if ($tmdbId) { $q=$pdo->prepare('SELECT id FROM episodes WHERE tmdb_id=?'); $q->execute([$tmdbId]); $id=$q->fetchColumn(); }
if (!$id) { $q=$pdo->prepare('SELECT id FROM episodes WHERE season_id=? AND episode_number=?'); $q->execute([$seasonId,$epNo]); $id=$q->fetchColumn(); }
}
resp(['ok'=>true,'id'=>(int)$id]);
}
case 'upsert_movie': {
$tmdb = $in['tmdb'] ?? null; if (!$tmdb || !isset($tmdb['id'])) fail('missing tmdb payload');
$stmt = $pdo->prepare("INSERT INTO movies (tmdb_id, title, original_title, release_year, poster_path, backdrop_path, runtime, json)
VALUES (?,?,?,?,?,?,?,?)
ON DUPLICATE KEY UPDATE title=VALUES(title), original_title=VALUES(original_title), release_year=VALUES(release_year), poster_path=VALUES(poster_path), backdrop_path=VALUES(backdrop_path), runtime=VALUES(runtime), json=VALUES(json)");
$stmt->execute([
$tmdb['id'],
$tmdb['title'] ?? $tmdb['name'] ?? '',
$tmdb['original_title'] ?? $tmdb['original_name'] ?? null,
isset($tmdb['release_date']) ? intval(substr($tmdb['release_date'],0,4)) : null,
$tmdb['poster_path'] ?? null,
$tmdb['backdrop_path'] ?? null,
$tmdb['runtime'] ?? null,
json_encode($tmdb, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),
]);
$id = $pdo->lastInsertId();
if (!$id) { $q=$pdo->prepare('SELECT id FROM movies WHERE tmdb_id=?'); $q->execute([$tmdb['id']]); $id=$q->fetchColumn(); }
resp(['ok'=>true,'id'=>(int)$id]);
}
case 'set_status': {
$type=$in['type'] ?? null;
$ref=(int)($in['ref_id'] ?? 0);
$st=$in['status'] ?? null; // may be string or int
if (!in_array($type,['movie','episode'],true) || !$ref || $st===null) fail('bad params');
// Map string statuses to int codes: 0 Init, 1 Progress, 2 Done
$stInt = is_numeric($st) ? (int)$st : (function($s){
$s = strtolower((string)$s);
if ($s==='progress') return 1; if ($s==='done') return 2; return 0;
})($st);
if ($type==='movie') {
$stmt=$pdo->prepare("UPDATE movies SET status=? WHERE id=?");
$stmt->execute([$stInt,$ref]);
} else {
$stmt=$pdo->prepare("UPDATE episodes SET status=? WHERE id=?");
$stmt->execute([$stInt,$ref]);
}
resp(['ok'=>true]);
}
case 'get_list': {
$type=$in['type'] ?? 'movie';
$status=$in['status'] ?? null;
$q=$in['q'] ?? '';
$limit=max(1,(int)($in['limit']??50));
$offset=max(0,(int)($in['offset']??0));
if ($type === 'episode') {
$statusVal = null;
if ($status) {
$s = strtolower($status);
if ($s==='init') $statusVal = 0; elseif ($s==='progress') $statusVal = 1; elseif ($s==='done') $statusVal = 2;
}
$base = "FROM episodes e
JOIN seasons se ON se.id = e.season_id
JOIN shows sh ON sh.id = se.show_id
WHERE 1=1";
// Prefer extracting show status directly from JSON and include cliffhanger column if present
$selectWithYearJson = "SELECT e.*,
CASE e.status WHEN 1 THEN 'Progress' WHEN 2 THEN 'Done' ELSE 'Init' END AS status,
sh.resolution AS resolution,
sh.poster_path AS poster_path,
sh.first_air_year AS first_air_year,
JSON_UNQUOTE(JSON_EXTRACT(sh.json, '$.status')) AS show_status,
sh.cliffhanger AS show_cliffhanger,
sh.json AS show_json,
se.season_number, sh.name AS show_name ".$base;
$selectNoYearJson = "SELECT e.*,
CASE e.status WHEN 1 THEN 'Progress' WHEN 2 THEN 'Done' ELSE 'Init' END AS status,
sh.resolution AS resolution,
sh.poster_path AS poster_path,
NULL AS first_air_year,
JSON_UNQUOTE(JSON_EXTRACT(sh.json, '$.status')) AS show_status,
sh.cliffhanger AS show_cliffhanger,
sh.json AS show_json,
se.season_number, sh.name AS show_name ".$base;
// Fallback selects without JSON_EXTRACT (older MySQL) — frontend will parse from show_json
$selectWithYear = "SELECT e.*,
CASE e.status WHEN 1 THEN 'Progress' WHEN 2 THEN 'Done' ELSE 'Init' END AS status,
sh.resolution AS resolution,
sh.poster_path AS poster_path,
sh.first_air_year AS first_air_year,
sh.cliffhanger AS show_cliffhanger,
sh.json AS show_json,
se.season_number, sh.name AS show_name ".$base;
$selectNoYear = "SELECT e.*,
CASE e.status WHEN 1 THEN 'Progress' WHEN 2 THEN 'Done' ELSE 'Init' END AS status,
sh.resolution AS resolution,
sh.poster_path AS poster_path,
NULL AS first_air_year,
sh.cliffhanger AS show_cliffhanger,
sh.json AS show_json,
se.season_number, sh.name AS show_name ".$base;
$run = function(string $sql) use ($pdo, $statusVal, $q, $offset, $limit) {
$params = [];
if ($statusVal !== null) { $sql .= " AND e.status = ?"; $params[] = $statusVal; }
if ($q) { $sql .= " AND e.name LIKE ?"; $params[] = "%$q%"; }
$sql .= " ORDER BY sh.name ASC, se.season_number ASC, e.episode_number ASC LIMIT ?, ?";
$params[] = $offset; $params[] = $limit;
$stmt = $pdo->prepare($sql);
$i=1; foreach ($params as $p) { $stmt->bindValue($i++, $p, is_int($p)?PDO::PARAM_INT:PDO::PARAM_STR); }
$stmt->execute();
return $stmt->fetchAll();
};
try {
$rows = $run($selectWithYearJson);
resp(['ok'=>true,'items'=>$rows]);
} catch (Throwable $e) {
$msg = $e->getMessage();
if (strpos($msg, 'Unknown column') !== false) {
// Maybe first_air_year missing — try JSON version without year
try {
$rows = $run($selectNoYearJson);
resp(['ok'=>true,'items'=>$rows]);
} catch (Throwable $e2) {
$msg2 = $e2->getMessage();
if (stripos($msg2, 'JSON_EXTRACT') !== false || stripos($msg2, 'Unknown function') !== false) {
// Fallback to non-JSON_EXTRACT selects
try {
$rows = $run($selectWithYear);
resp(['ok'=>true,'items'=>$rows]);
} catch (Throwable $e3) {
if (strpos($e3->getMessage(), 'Unknown column') !== false) {
$rows = $run($selectNoYear);
resp(['ok'=>true,'items'=>$rows]);
} else { throw $e3; }
}
} else { throw $e2; }
}
} elseif (stripos($msg, 'JSON_EXTRACT') !== false || stripos($msg, 'Unknown function') !== false) {
// JSON functions not available
try {
$rows = $run($selectWithYear);
resp(['ok'=>true,'items'=>$rows]);
} catch (Throwable $e4) {
if (strpos($e4->getMessage(), 'Unknown column') !== false) {
$rows = $run($selectNoYear);
resp(['ok'=>true,'items'=>$rows]);
} else { throw $e4; }
}
} else { throw $e; }
}
}
if ($type==='movie') {
$statusVal = null;
if ($status) {
$s = strtolower($status);
if ($s==='init') $statusVal = 0; elseif ($s==='progress') $statusVal = 1; elseif ($s==='done') $statusVal = 2;
}
$sql = "SELECT m.*, CASE m.status WHEN 1 THEN 'Progress' WHEN 2 THEN 'Done' ELSE 'Init' END AS status, m.resolution
FROM movies m WHERE 1=1";
$params=[];
if ($statusVal !== null) { $sql.=" AND m.status=?"; $params[]=$statusVal; }
if ($q){$sql.=" AND m.title LIKE ?"; $params[]='%'.$q.'%';}
$sql.=" ORDER BY m.title ASC LIMIT ?,?"; $params[]=$offset; $params[]=$limit;
$stmt=$pdo->prepare($sql); $i=1; foreach($params as $p){$stmt->bindValue($i++,$p,is_int($p)?PDO::PARAM_INT:PDO::PARAM_STR);} $stmt->execute();
resp(['ok'=>true,'items'=>$stmt->fetchAll()]);
}
fail('unsupported type');
}
case 'get_show_by_tmdb': {
$tmdbId = (int)($in['tmdb_id'] ?? 0);
if (!$tmdbId) fail('bad params');
$stmt = $pdo->prepare('SELECT id FROM shows WHERE tmdb_id = ?');
$stmt->execute([$tmdbId]);
$id = $stmt->fetchColumn();
resp(['ok' => true, 'id' => $id ? (int)$id : null]);
}
case 'ping': {
resp([
'ok' => true,
'time' => date('c'),
'origin' => $origin,
'php' => PHP_VERSION,
]);
}
default: fail('unknown action');
}
} catch (Throwable $e) {
if (MM_DEBUG) resp(['ok'=>false,'error'=>$e->getMessage()], 500);
resp(['ok'=>false,'error'=>'server error'], 500);
}

@ -0,0 +1,509 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
url: "https://pub.dev"
source: hosted
version: "2.11.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
cached_network_image:
dependency: "direct main"
description:
name: cached_network_image
sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916"
url: "https://pub.dev"
source: hosted
version: "3.4.1"
cached_network_image_platform_interface:
dependency: transitive
description:
name: cached_network_image_platform_interface
sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829"
url: "https://pub.dev"
source: hosted
version: "4.1.1"
cached_network_image_web:
dependency: transitive
description:
name: cached_network_image_web
sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
characters:
dependency: transitive
description:
name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
clock:
dependency: transitive
description:
name: clock
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
url: "https://pub.dev"
source: hosted
version: "1.1.1"
collection:
dependency: transitive
description:
name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
url: "https://pub.dev"
source: hosted
version: "1.18.0"
crypto:
dependency: transitive
description:
name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "https://pub.dev"
source: hosted
version: "3.0.6"
dio:
dependency: "direct main"
description:
name: dio
sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9
url: "https://pub.dev"
source: hosted
version: "5.9.0"
dio_web_adapter:
dependency: transitive
description:
name: dio_web_adapter
sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
ffi:
dependency: transitive
description:
name: ffi
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_cache_manager:
dependency: transitive
description:
name: flutter_cache_manager
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
url: "https://pub.dev"
source: hosted
version: "3.4.1"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
url: "https://pub.dev"
source: hosted
version: "5.0.0"
flutter_riverpod:
dependency: "direct main"
description:
name: flutter_riverpod
sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1"
url: "https://pub.dev"
source: hosted
version: "2.6.1"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
http:
dependency: transitive
description:
name: http
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
url: "https://pub.dev"
source: hosted
version: "1.5.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
url: "https://pub.dev"
source: hosted
version: "4.0.2"
intl:
dependency: "direct main"
description:
name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.dev"
source: hosted
version: "0.19.0"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
url: "https://pub.dev"
source: hosted
version: "10.0.5"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
url: "https://pub.dev"
source: hosted
version: "3.0.5"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
lints:
dependency: transitive
description:
name: lints
sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413"
url: "https://pub.dev"
source: hosted
version: "5.0.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://pub.dev"
source: hosted
version: "0.12.16+1"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
version: "0.11.1"
meta:
dependency: transitive
description:
name: meta
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
url: "https://pub.dev"
source: hosted
version: "1.15.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
octo_image:
dependency: transitive
description:
name: octo_image
sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
path:
dependency: transitive
description:
name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://pub.dev"
source: hosted
version: "1.9.0"
path_provider:
dependency: transitive
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2"
url: "https://pub.dev"
source: hosted
version: "2.2.15"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
riverpod:
dependency: transitive
description:
name: riverpod
sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959"
url: "https://pub.dev"
source: hosted
version: "2.6.1"
rxdart:
dependency: transitive
description:
name: rxdart
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
url: "https://pub.dev"
source: hosted
version: "0.28.0"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_span:
dependency: transitive
description:
name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.dev"
source: hosted
version: "1.10.0"
sprintf:
dependency: transitive
description:
name: sprintf
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
sqflite:
dependency: transitive
description:
name: sqflite
sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
sqflite_android:
dependency: transitive
description:
name: sqflite_android
sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709"
url: "https://pub.dev"
source: hosted
version: "2.5.4+6"
sqflite_darwin:
dependency: transitive
description:
name: sqflite_darwin
sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c"
url: "https://pub.dev"
source: hosted
version: "2.4.1+1"
sqflite_platform_interface:
dependency: transitive
description:
name: sqflite_platform_interface
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
url: "https://pub.dev"
source: hosted
version: "1.11.1"
state_notifier:
dependency: transitive
description:
name: state_notifier
sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb
url: "https://pub.dev"
source: hosted
version: "1.0.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.dev"
source: hosted
version: "2.1.2"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
synchronized:
dependency: transitive
description:
name: synchronized
sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225"
url: "https://pub.dev"
source: hosted
version: "3.3.0+3"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
url: "https://pub.dev"
source: hosted
version: "1.2.1"
test_api:
dependency: transitive
description:
name: test_api
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
url: "https://pub.dev"
source: hosted
version: "0.7.2"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
uuid:
dependency: transitive
description:
name: uuid
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
url: "https://pub.dev"
source: hosted
version: "4.5.1"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
url: "https://pub.dev"
source: hosted
version: "14.2.5"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
sdks:
dart: ">=3.5.0 <4.0.0"
flutter: ">=3.24.0"

@ -0,0 +1,25 @@
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

@ -0,0 +1,14 @@
@echo off
setlocal
REM Wrapper to run export.ps1 with double-click or CLI
set SCRIPT_DIR=%~dp0
powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%export.ps1" %*
set EXITCODE=%ERRORLEVEL%
if %EXITCODE% NEQ 0 (
echo Export failed. Exit code %EXITCODE%.
exit /b %EXITCODE%
)
echo Archive created. Check the dist\ folder.
endlocal

@ -0,0 +1,133 @@
Param(
[switch]$Clean,
[switch]$Minimal,
[switch]$AsFolder,
[switch]$Bundle,
[int]$Chunk = 0,
[string]$OutDir = "dist",
[string]$Name = "multimediaFlutter-src"
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# Ensure output and temp directories
if (-not (Test-Path $OutDir)) {
New-Item -ItemType Directory -Path $OutDir | Out-Null
}
$timestamp = Get-Date -Format 'yyyyMMdd-HHmm'
$baseName = "$Name-$timestamp"
$zipPath = Join-Path $OutDir ("$baseName.zip")
$tempDir = Join-Path ([IO.Path]::GetTempPath()) ("mf_export_" + [Guid]::NewGuid().ToString('N'))
if (-not (Test-Path $tempDir)) { New-Item -ItemType Directory -Path $tempDir -Force | Out-Null }
$stageDir = Join-Path $tempDir 'stage'
if (-not (Test-Path $stageDir)) { New-Item -ItemType Directory -Path $stageDir -Force | Out-Null }
try {
if ($Clean) {
try {
Write-Host "Running 'flutter clean'..." -ForegroundColor Cyan
flutter clean | Out-Null
} catch {
Write-Warning "'flutter clean' failed or Flutter not in PATH. Continuing."
}
}
Write-Host "Staging files to temp folder..." -ForegroundColor Cyan
if ($Minimal) {
# Copy a minimal set for code review in ChatGPT
$includeDirs = @('lib','test','assets')
foreach ($dir in $includeDirs) {
if (Test-Path $dir) {
Copy-Item $dir -Destination (Join-Path $stageDir $dir) -Recurse -Force
}
}
$rootFiles = @('pubspec.yaml','analysis_options.yaml','README.md')
foreach ($f in $rootFiles) {
if (Test-Path $f) { Copy-Item $f -Destination (Join-Path $stageDir $f) -Force }
}
if (Test-Path 'web') {
New-Item -ItemType Directory -Force -Path (Join-Path $stageDir 'web') | Out-Null
foreach ($wf in @('index.html','manifest.json')) {
if (Test-Path (Join-Path 'web' $wf)) {
Copy-Item (Join-Path 'web' $wf) -Destination (Join-Path $stageDir (Join-Path 'web' $wf)) -Force
}
}
}
} else {
# Use robocopy to copy everything except heavy/generated folders
$excludeDirs = @('build', '.dart_tool', 'ios\Pods', 'android\build', '.idea', '.vscode', 'web\build', 'android\.gradle', 'dist', 'tool', '.git', '.github')
$xdArgs = @()
foreach ($d in $excludeDirs) { $xdArgs += @('/XD', $d) }
# robocopy exit codes: treat 0-7 as success
$robocopyCmd = @('robocopy', '.', $stageDir, '/E') + $xdArgs
$p = Start-Process -FilePath $robocopyCmd[0] -ArgumentList $robocopyCmd[1..($robocopyCmd.Length-1)] -Wait -PassThru -NoNewWindow
if ($p.ExitCode -gt 7) {
throw "Robocopy failed with exit code $($p.ExitCode)"
}
}
# Option A: Single bundled text file (bypasses 10-file limit)
if ($Bundle) {
$bundlePath = Join-Path $OutDir ("$baseName-bundle.txt")
if (Test-Path $bundlePath) { Remove-Item $bundlePath -Force }
Write-Host "Creating bundled text: $bundlePath" -ForegroundColor Cyan
$textExt = @('.dart','.yaml','.yml','.md','.json','.html','.htm','.css','.js','.xml','.gradle','.properties','.kt','.swift','.plist','.sh','.bat','.ps1','.txt','.php')
$files = Get-ChildItem -Path $stageDir -Recurse -File | Where-Object { $textExt -contains ([IO.Path]::GetExtension($_.Name).ToLower()) }
foreach ($f in $files) {
$rel = $f.FullName.Substring($stageDir.Length + 1)
"===== BEGIN FILE: $rel =====" | Out-File -FilePath $bundlePath -Append -Encoding utf8
try { (Get-Content -Raw -Path $f.FullName) | Out-File -FilePath $bundlePath -Append -Encoding utf8 } catch { "[binary or unreadable content omitted]" | Out-File -FilePath $bundlePath -Append -Encoding utf8 }
"\n===== END FILE =====\n" | Out-File -FilePath $bundlePath -Append -Encoding utf8
}
Write-Host "Done. Upload this single file to your ChatGPT Project:" -ForegroundColor Green
Write-Host " $bundlePath"
}
elseif ($Chunk -gt 0) {
# Option B: Chunk into folder packs of N files
$outBase = Join-Path $OutDir $baseName
if (Test-Path $outBase) { Remove-Item $outBase -Recurse -Force }
New-Item -ItemType Directory -Path $outBase | Out-Null
$allFiles = Get-ChildItem -Path $stageDir -Recurse -File
if ($allFiles.Count -eq 0) { throw "No files found to export." }
$pack = 1
$inPack = 0
$packDir = Join-Path $outBase ("pack-" + $pack.ToString('000'))
New-Item -ItemType Directory -Path $packDir | Out-Null
foreach ($file in $allFiles) {
if ($inPack -ge $Chunk) {
$pack++
$inPack = 0
$packDir = Join-Path $outBase ("pack-" + $pack.ToString('000'))
New-Item -ItemType Directory -Path $packDir | Out-Null
}
$rel = $file.FullName.Substring($stageDir.Length + 1)
$destPath = Join-Path $packDir $rel
$destDir = Split-Path -Parent $destPath
if (-not (Test-Path $destDir)) { New-Item -ItemType Directory -Path $destDir -Force | Out-Null }
Copy-Item $file.FullName -Destination $destPath -Force
$inPack++
}
Write-Host "Done. Upload each pack folder (<= $Chunk files) one by one:" -ForegroundColor Green
Write-Host " $outBase"
}
elseif ($AsFolder) {
$outFolder = Join-Path $OutDir $baseName
if (Test-Path $outFolder) { Remove-Item $outFolder -Recurse -Force }
Write-Host "Preparing folder for drag-and-drop: $outFolder" -ForegroundColor Cyan
if (-not (Test-Path $outFolder)) { New-Item -ItemType Directory -Path $outFolder | Out-Null }
Copy-Item (Join-Path $stageDir '*') -Destination $outFolder -Recurse -Force
Write-Host "Done. Drag-and-drop this folder into your ChatGPT Project:" -ForegroundColor Green
Write-Host " $outFolder"
} else {
Write-Host "Creating archive: $zipPath" -ForegroundColor Cyan
if (Test-Path $zipPath) { Remove-Item $zipPath -Force }
Compress-Archive -Path (Join-Path $stageDir '*') -DestinationPath $zipPath -Force
Write-Host "Done. Upload this file to your ChatGPT Project:" -ForegroundColor Green
Write-Host " $zipPath"
}
} finally {
if (Test-Path $tempDir) { Remove-Item $tempDir -Recurse -Force }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@ -0,0 +1,38 @@
<!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>

@ -0,0 +1,35 @@
{
"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"
}
]
}
Loading…
Cancel
Save