Erster Upload von MX Linux

This commit is contained in:
2026-02-02 09:45:14 +01:00
commit a25d0becaf
109 changed files with 6801 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

101
android/.gitignore vendored Normal file
View File

@@ -0,0 +1,101 @@
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
# Built application files
*.apk
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
# google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
# Android Profiling
*.hprof
# Cordova plugins for Capacitor
capacitor-cordova-android-plugins
# Copied web assets
app/src/main/assets/public
# Generated Config files
app/src/main/assets/capacitor.config.json
app/src/main/assets/capacitor.plugins.json
app/src/main/res/xml/config.xml

2
android/app/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/build/*
!/build/.npmkeep

54
android/app/build.gradle Normal file
View File

@@ -0,0 +1,54 @@
apply plugin: 'com.android.application'
android {
namespace = "com.medienstation.app"
compileSdk = rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "com.medienstation.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
repositories {
flatDir{
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
implementation project(':capacitor-android')
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
implementation project(':capacitor-cordova-android-plugins')
}
apply from: 'capacitor.build.gradle'
try {
def servicesJSON = file('google-services.json')
if (servicesJSON.text) {
apply plugin: 'com.google.gms.google-services'
}
} catch(Exception e) {
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
}

View File

@@ -0,0 +1,21 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
}
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':capacitor-camera')
implementation project(':capacitor-filesystem')
implementation "com.android.support:support-v4:28.+"
implementation "androidx.legacy:legacy-support-v4:1.0.0"
}
if (hasProperty('postBuildExtras')) {
postBuildExtras()
}

21
android/app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,26 @@
package com.getcapacitor.myapp;
import static org.junit.Assert.*;
import android.content.Context;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.getcapacitor.app", appContext.getPackageName());
}
}

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.medienstation.app">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:requestLegacyExternalStorage="true"
tools:replace="android:appComponentFactory"
android:appComponentFactory="androidx.core.app.CoreComponentFactory">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:name=".MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
</provider>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -0,0 +1,56 @@
package com.medienstation.app; // Das MUSS die erste Zeile sein
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Base64;
import androidx.print.PrintHelper;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;
@CapacitorPlugin(name = "DirectPrinter")
public class DirectPrinterPlugin extends Plugin {
@PluginMethod
public void printBase64(PluginCall call) {
String base64String = call.getString("data");
String jobName = call.getString("name", "News-Print-Job");
if (base64String == null) {
call.reject("Keine Daten empfangen");
return;
}
try {
// 1. Base64 Header entfernen
if (base64String.contains(",")) {
base64String = base64String.split(",")[1];
}
// 2. String in Bytes umwandeln
byte[] decodedString = Base64.decode(base64String, Base64.DEFAULT);
// 3. Bytes in ein Bitmap (Bild) umwandeln
Bitmap bitmap = BitmapFactory.decodeByteArray(decodedString, 0, decodedString.length);
if (bitmap == null) {
call.reject("Bild konnte nicht dekodiert werden");
return;
}
// 4. Drucken auf dem UI Thread
getActivity().runOnUiThread(() -> {
PrintHelper photoPrinter = new PrintHelper(getContext());
photoPrinter.setScaleMode(PrintHelper.SCALE_MODE_FIT);
photoPrinter.printBitmap(jobName, bitmap);
call.resolve();
});
} catch (Exception e) {
call.reject("Fehler beim Drucken: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,29 @@
package com.medienstation.app;
import android.os.Bundle;
import com.getcapacitor.BridgeActivity;
import android.view.WindowManager;
import android.webkit.WebView;
import android.webkit.WebSettings; // <--- NEU (+)
public class MainActivity extends BridgeActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
registerPlugin(DirectPrinterPlugin.class);
super.onCreate(savedInstanceState);
// 1. Display anlassen
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
// 2. WebView Einstellungen holen
WebView webView = getBridge().getWebView();
WebSettings settings = webView.getSettings();
// 3. WICHTIG: Autoplay erlauben! (+)
settings.setMediaPlaybackRequiresUserGesture(false);
// 4. Debugging
WebView.setWebContentsDebuggingEnabled(true);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>

View File

@@ -0,0 +1,36 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="512"
android:viewportHeight="512">
<group android:scaleX="0.92"
android:scaleY="0.92"
android:translateX="20.48"
android:translateY="20.48">
<path
android:pathData="M100,0L412,0A100,100 0,0 1,512 100L512,412A100,100 0,0 1,412 512L100,512A100,100 0,0 1,0 412L0,100A100,100 0,0 1,100 0z"
android:fillColor="#0f172a"/>
<path
android:pathData="M256,120C256,120 360,120 390,200L256,256V120Z"
android:fillColor="#facc15"/>
<path
android:pathData="M390,200C390,200 420,320 320,390L256,256L390,200Z"
android:fillColor="#ef4444"/>
<path
android:pathData="M320,390C320,390 200,420 122,312L256,256L320,390Z"
android:fillColor="#3b82f6"/>
<path
android:pathData="M122,312C122,312 90,180 190,120L256,256L122,312Z"
android:fillColor="#10b981"/>
<path
android:pathData="M256,256m-50,0a50,50 0,1 1,100 0a50,50 0,1 1,-100 0"
android:fillColor="#ffffff"/>
<path
android:pathData="M256,256m-35,0a35,35 0,1 1,70 0a35,35 0,1 1,-70 0"
android:fillColor="#1e293b"/>
<path
android:pathData="M270,240m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0"
android:fillColor="#ffffff"
android:fillAlpha="0.8"/>
</group>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

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

View File

@@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="app_name">MedienStation</string>
<string name="title_activity_main">MedienStation</string>
<string name="package_name">com.medienstation.app</string>
<string name="custom_url_scheme">com.medienstation.app</string>
</resources>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:background">@null</item>
</style>
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
<item name="android:background">@drawable/splash</item>
</style>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
</paths>

View File

@@ -0,0 +1,18 @@
package com.getcapacitor.myapp;
import static org.junit.Assert.*;
import org.junit.Test;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}

29
android/build.gradle Normal file
View File

@@ -0,0 +1,29 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.13.2'
classpath 'com.google.gms:google-services:4.4.4'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
apply from: "variables.gradle"
allprojects {
repositories {
google()
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

View File

@@ -0,0 +1,9 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../../node_modules/@capacitor/android/capacitor')
include ':capacitor-camera'
project(':capacitor-camera').projectDir = new File('../node_modules/@capacitor/camera/android')
include ':capacitor-filesystem'
project(':capacitor-filesystem').projectDir = new File('../node_modules/@capacitor/filesystem/android')

24
android/gradle.properties Normal file
View File

@@ -0,0 +1,24 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
android.useAndroidX=true
android.enableJetifier=true

Binary file not shown.

View File

@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

251
android/gradlew vendored Executable file
View File

@@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# 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
if ! command -v java >/dev/null 2>&1
then
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
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
android/gradlew.bat vendored Normal file
View File

@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@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
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@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="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 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!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

5
android/settings.gradle Normal file
View File

@@ -0,0 +1,5 @@
include ':app'
include ':capacitor-cordova-android-plugins'
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
apply from: 'capacitor.settings.gradle'

16
android/variables.gradle Normal file
View File

@@ -0,0 +1,16 @@
ext {
minSdkVersion = 24
compileSdkVersion = 36
targetSdkVersion = 36
androidxActivityVersion = '1.11.0'
androidxAppCompatVersion = '1.7.1'
androidxCoordinatorLayoutVersion = '1.3.0'
androidxCoreVersion = '1.17.0'
androidxFragmentVersion = '1.8.9'
coreSplashScreenVersion = '1.2.0'
androidxWebkitVersion = '1.14.0'
junitVersion = '4.13.2'
androidxJunitVersion = '1.3.0'
androidxEspressoCoreVersion = '3.7.0'
cordovaAndroidVersion = '14.0.1'
}

6
capacitor.config.json Normal file
View File

@@ -0,0 +1,6 @@
{
"appId": "com.medienstation.app",
"appName": "MedienStation",
"webDir": "public",
"bundledWebRuntime": false
}

231
index.html Normal file
View File

@@ -0,0 +1,231 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>MedienStation Hub</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<link rel="stylesheet" href="js/tailwind.css">
<script src="https://cdn.tailwindcss.com"></script>
<style>
/* Globaler Reset */
html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; }
body {
background-color: #0f172a;
color: white;
font-family: sans-serif;
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
}
.font-comic { font-family: 'Comic Sans MS', 'Chalkboard SE', sans-serif; }
.app-card { cursor: pointer; transition: transform 0.1s; }
.app-card:active { transform: scale(0.95); filter: brightness(1.2); }
.no-scrollbar::-webkit-scrollbar { display: none; }
.fade-in { animation: fadeIn 0.5s ease-out; }
@keyframes fadeIn { from { opacity: 0; transform: scale(0.98); } to { opacity: 1; transform: scale(1); } }
/* --- FINALER FIX v7.1: GRID + GHOST MODE --- */
#qr-modal {
/* GRID zwingt den Inhalt mathematisch in die Mitte */
display: grid !important;
place-items: center;
position: fixed;
top: 0; left: 0; width: 100vw; height: 100vh;
z-index: 99999;
background-color: rgba(0,0,0,0.85);
backdrop-filter: blur(5px);
/* GHOST MODE: Das Element ist da, aber unsichtbar.
So kann der Browser es berechnen, bevor man es sieht. */
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease-in-out;
}
/* Sichtbar schalten */
#qr-modal.modal-visible {
opacity: 1;
pointer-events: auto;
}
</style>
</head>
<body class="p-4 box-border fade-in relative" onpointerdown="resetHubTimer()" ontouchstart="resetHubTimer()">
<button onclick="toggleInfo(); playSound('click')" class="absolute top-6 left-6 z-50 w-12 h-12 bg-white/10 text-white rounded-full font-bold border-2 border-white/30 backdrop-blur-md flex items-center justify-center shadow-lg active:scale-95">
<span class="text-2xl">?</span>
</button>
<div class="grid grid-cols-12 grid-rows-12 gap-3 h-full w-full pb-28">
<div class="col-span-12 row-span-3 bg-slate-800 rounded-3xl p-6 flex justify-between items-center shadow-lg border border-slate-700 relative overflow-hidden group pl-24">
<div class="absolute -right-10 -top-10 w-64 h-64 bg-yellow-400 rounded-full blur-3xl opacity-10 group-hover:opacity-20 transition-opacity duration-500"></div>
<div class="z-10 w-2/3 select-none" onclick="triggerAdmin()">
<h3 class="text-slate-400 text-sm font-bold tracking-widest uppercase mb-1 cursor-pointer">Deine Mission</h3>
<div id="mission-text" class="text-2xl md:text-4xl lg:text-5xl font-black text-white leading-tight transition-all duration-300">Wähle eine App! 👉</div>
</div>
<button onclick="playSound('click'); newMission()" class="z-10 bg-yellow-400 hover:bg-yellow-300 text-black font-black text-xl py-4 px-10 rounded-full shadow-lg active:translate-y-2 transition-all transform hover:scale-105">
🎲 WÜRFELN
</button>
</div>
<div onclick="openApp('apps/sound.html', 'Sound')" class="app-card col-span-4 row-span-5 bg-gradient-to-br from-rose-500 to-rose-600 rounded-3xl flex flex-col items-center justify-center shadow-lg border-b-8 border-rose-800 active:border-b-0 group">
<div class="text-6xl mb-2 drop-shadow-md group-hover:-translate-y-2 transition-transform">🎹</div>
<h3 class="text-2xl font-bold">Sound</h3>
</div>
<div onclick="openApp('apps/rec.html', 'Mikro')" class="app-card col-span-2 row-span-5 bg-gradient-to-br from-red-600 to-red-700 rounded-3xl flex flex-col items-center justify-center shadow-lg border-b-8 border-red-900 active:border-b-0">
<div class="text-5xl mb-2 drop-shadow-md">🎙️</div>
<h3 class="text-xl font-bold">Mikro</h3>
</div>
<div onclick="openApp('apps/gif.html', 'Loop')" class="app-card col-span-3 row-span-5 bg-gradient-to-br from-sky-500 to-sky-600 rounded-3xl flex flex-col items-center justify-center shadow-lg border-b-8 border-sky-800 active:border-b-0 group">
<div class="text-6xl mb-2 drop-shadow-md group-hover:rotate-12 transition-transform">📹</div>
<h3 class="text-2xl font-bold">Loop</h3>
</div>
<div onclick="openApp('apps/magic.html', 'Magic')" class="app-card col-span-3 row-span-5 bg-gradient-to-br from-violet-500 to-violet-600 rounded-3xl flex flex-col items-center justify-center shadow-lg border-b-8 border-violet-800 active:border-b-0">
<div class="text-6xl mb-2 drop-shadow-md"></div>
<h3 class="text-2xl font-bold">Magic</h3>
</div>
<div onclick="openApp('apps/pixel.html', 'Pixel')" class="app-card col-span-4 row-span-4 bg-gradient-to-br from-emerald-500 to-emerald-700 rounded-3xl flex flex-col items-center justify-center shadow-lg border-b-8 border-emerald-900 active:border-b-0">
<div class="text-6xl mb-2 drop-shadow-md font-mono">👾</div>
<h3 class="text-2xl font-bold">Pixel</h3>
</div>
<div onclick="openApp('apps/news.html', 'News')" class="app-card col-span-4 row-span-4 bg-gradient-to-br from-blue-700 to-blue-900 rounded-3xl flex flex-col items-center justify-center shadow-lg border-b-8 border-blue-950 active:border-b-0 relative overflow-hidden">
<div class="text-5xl mb-2 drop-shadow-md relative z-10">📰</div>
<h3 class="text-xl font-bold relative z-10">News</h3>
</div>
<div onclick="openApp('apps/comic.html', 'Comic')" class="app-card col-span-4 row-span-4 bg-gradient-to-r from-yellow-400 to-orange-400 rounded-3xl flex items-center justify-center gap-4 shadow-lg border-b-8 border-orange-600 active:border-b-0 group">
<div class="text-6xl drop-shadow-md transform -rotate-6 transition-transform group-hover:rotate-0">📸</div>
<div class="text-left text-black"><h3 class="text-3xl font-black font-comic tracking-wide">COMIC</h3></div>
</div>
</div>
<div class="fixed bottom-6 w-full flex flex-col items-center justify-center pointer-events-none z-40">
<button onclick="toggleQR()" class="pointer-events-auto bg-slate-800/90 backdrop-blur border border-slate-600 px-8 py-3 rounded-full shadow-2xl flex items-center gap-4 hover:bg-slate-700 hover:scale-105 hover:border-white transition-all">
<img src="assets/logo.png" class="h-12 w-auto object-contain" alt="Logo" onerror="this.style.display='none'">
<div class="flex flex-col text-left">
<span class="text-[10px] text-slate-400 font-bold uppercase tracking-widest leading-none mb-1">Ein Projekt der</span>
<span class="text-lg font-black text-white leading-none tracking-wide">AV-MEDIENZENTRALE</span>
</div>
</button>
</div>
<div id="info-modal" class="hidden fixed inset-0 z-[200] bg-black/80 backdrop-blur-sm flex items-center justify-center p-4">
<div class="bg-slate-800 border-2 border-slate-600 rounded-[2rem] max-w-4xl w-full p-8 shadow-2xl relative">
<button onclick="toggleInfo(); playSound('click')" class="absolute top-6 right-6 text-white text-4xl font-bold"></button>
<h2 class="text-4xl font-black text-white mb-8 border-b border-slate-600 pb-4">GUIDE</h2>
<div class="grid grid-cols-2 gap-4 text-base">
<div class="p-4 bg-slate-700/50 rounded-2xl flex items-center gap-4"><div class="text-3xl">🎹</div><div><h3 class="text-rose-400 font-bold">Sound</h3><p class="text-slate-300 text-sm">Beats & Loops.</p></div></div>
<div class="p-4 bg-slate-700/50 rounded-2xl flex items-center gap-4"><div class="text-3xl">📹</div><div><h3 class="text-sky-400 font-bold">Loop</h3><p class="text-slate-300 text-sm">Video-Loops.</p></div></div>
<div class="p-4 bg-slate-700/50 rounded-2xl flex items-center gap-4"><div class="text-3xl"></div><div><h3 class="text-violet-400 font-bold">Magic</h3><p class="text-slate-300 text-sm">Green Screen.</p></div></div>
<div class="p-4 bg-slate-700/50 rounded-2xl flex items-center gap-4"><div class="text-3xl">📸</div><div><h3 class="text-orange-400 font-bold">Comic</h3><p class="text-slate-300 text-sm">Foto Storys.</p></div></div>
</div>
</div>
</div>
<div id="qr-modal" onclick="toggleQR()">
<div onclick="event.stopPropagation()"
style="width: 90%; max-width: 400px; background: white; padding: 40px; border-radius: 30px; text-align: center; box-shadow: 0 20px 50px rgba(0,0,0,0.5);">
<h3 style="color: #0f172a; font-size: 24px; font-weight: 900; text-transform: uppercase; margin: 0 0 20px 0;">Besuche uns!</h3>
<div style="width: 250px; height: 250px; background: #f8fafc; border: 2px solid #e2e8f0; border-radius: 15px; margin: 0 auto 20px auto; display: flex; align-items: center; justify-content: center;">
<img src="assets/qr.png" alt="QR Code" style="width: 230px; height: 230px; object-fit: contain;"
onerror="this.src='data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iI2ZmZiIvPjx0ZXh0IHg9IjUwIiB5PSI1MCIgZm9udC1zaXplPSIxMCIgdGV4dC1hbmNob3I9Im1pZGRsZSI+UVIgQ29kZTwvdGV4dD48L3N2Zz4='">
</div>
<p style="color: #64748b; font-weight: bold; margin-bottom: 25px;">Scan den Code mit deinem Handy.</p>
<button onclick="toggleQR()"
style="width: 100%; background-color: #0f172a; color: white; border: none; padding: 15px; font-size: 18px; font-weight: 900; border-radius: 12px; cursor: pointer;">
SCHLIESSEN
</button>
</div>
</div>
<div id="admin-modal" class="hidden fixed inset-0 bg-black/90 z-[300] flex items-center justify-center p-4">
<div class="bg-slate-800 p-8 rounded-3xl border-2 border-slate-600 w-full max-w-lg">
<h2 class="text-3xl font-black text-white mb-6">⚙️ ADMIN MENU</h2>
<div class="grid grid-cols-2 gap-4">
<button onclick="location.reload()" class="bg-blue-600 text-white font-bold py-4 rounded-xl">🔄 Reload</button>
<button onclick="closeAdmin()" class="bg-slate-600 text-white font-bold py-4 rounded-xl">Zurück</button>
</div>
</div>
</div>
<script>
const missions = ["Erstelle ein Geräusch: Kälte ❄️", "Comic: Streit ums Brot 🥪", "Magic: Auf dem Mond 🚀", "Werbung: Unsichtbare Socken 🧦", "Loop: Hallo ohne Worte 👋", "Witz in 3 Bildern 🤡", "Selfie: Fliegen ✨", "Elefant mit Schluckauf 🐘", "Pixel-Monster Haustier 👾", "News: Ufo gelandet 👽"];
const playSound = (id) => {
const a = new Audio(`assets/sounds/${id}.mp3`);
a.volume = 1.0;
a.play().catch(() => {});
};
function openApp(url, appName) {
playSound('click');
setTimeout(() => { window.location.href = url; }, 300);
}
function newMission() {
const txt = document.getElementById('mission-text');
txt.innerText = missions[Math.floor(Math.random() * missions.length)];
}
let hubIdleTimer;
function resetHubTimer() {
clearTimeout(hubIdleTimer);
hubIdleTimer = setTimeout(() => {
document.getElementById('admin-modal').classList.add('hidden');
document.getElementById('info-modal').classList.add('hidden');
// Modal verstecken (Klasse entfernen)
document.getElementById('qr-modal').classList.remove('modal-visible');
document.getElementById('mission-text').innerText = "Wähle eine App! 👉";
tapCount = 0;
}, 60000);
}
resetHubTimer();
function toggleInfo() {
resetHubTimer();
const m = document.getElementById('info-modal');
m.classList.toggle('hidden');
m.classList.toggle('flex');
}
function toggleQR() {
resetHubTimer();
const modal = document.getElementById('qr-modal');
// GHOST TOGGLE: Sichtbarkeit umschalten
modal.classList.toggle('modal-visible');
playSound('click');
}
let tapCount = 0; let tapTimer;
function triggerAdmin() {
resetHubTimer(); tapCount++; clearTimeout(tapTimer); tapTimer = setTimeout(() => { tapCount = 0; }, 1000);
if (tapCount >= 5) {
tapCount = 0;
if (prompt("PIN:") === "1234") {
document.getElementById('admin-modal').classList.remove('hidden');
document.getElementById('admin-modal').classList.add('flex');
}
}
}
function closeAdmin() {
document.getElementById('admin-modal').classList.add('hidden');
document.getElementById('admin-modal').classList.remove('flex');
}
</script>
</body>
</html>

1165
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

23
package.json Normal file
View File

@@ -0,0 +1,23 @@
{
"name": "medien-station-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"postinstall": "jetifier",
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^7.2.4"
},
"dependencies": {
"@capacitor/camera": "^8.0.0",
"@capacitor/filesystem": "^8.0.0",
"awesome-cordova-plugins": "^5.40.0",
"cordova-plugin-printer": "^0.8.0",
"cordova-plugin-x-socialsharing": "^6.0.4",
"es6-promise-plugin": "^4.2.2"
}
}

174
public/apps/comic.html Normal file
View File

@@ -0,0 +1,174 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Comic Studio</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<script src="../js/tailwind.js"></script>
<script src="https://cdn.tailwindcss.com"></script> <style>
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
html {
width: 100%; height: 100%;
background-color: #facc15;
}
body {
/* Fixiertes Layout für maximale Stabilität */
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
margin: 0;
background-color: #facc15;
color: black;
overflow: hidden !important;
user-select: none;
touch-action: none;
font-family: sans-serif;
animation: fadeIn 0.4s ease-out;
transition: background-color 0.3s ease;
box-sizing: border-box;
/* OPTIMIERUNG: Näher an den Rand rutschen */
/* Oben: Notch + 10px (statt vorher 60px pauschal) */
padding-top: calc(10px + env(safe-area-inset-top));
/* Unten: Home-Balken + 20px */
padding-bottom: max(20px, env(safe-area-inset-bottom));
}
.font-comic { font-family: 'Comic Sans MS', 'Chalkboard SE', sans-serif; }
.no-scrollbar::-webkit-scrollbar { display: none; }
/* Sprechblasen */
.bubble-container { position: absolute; z-index: 20; display: flex; flex-direction: column; align-items: center; left: 50%; bottom: 25%; transform: translateX(-50%); }
.bubble-handle { background-color: #3b82f6; color: white; border-radius: 50%; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; cursor: move; box-shadow: 0 4px 6px rgba(0,0,0,0.3); margin-bottom: -15px; z-index: 30; font-size: 24px; touch-action: none; border: 2px solid white; }
.bubble-text { background: white; border: 4px solid black; border-radius: 2rem; padding: 1rem 1.5rem; min-width: 160px; max-width: 280px; text-align: center; font-family: 'Comic Sans MS', sans-serif; font-size: 1.5rem; line-height: 1.2; box-shadow: 8px 8px 0px rgba(0,0,0,0.2); outline: none; cursor: text; }
button:active { transform: scale(0.95); transition: transform 0.1s; }
@media print {
@page { size: landscape; margin: 0mm; }
body * { visibility: hidden; }
#print-container, #print-container * { visibility: visible; }
#print-container {
position: fixed; left: 0; top: 0; width: 100vw; height: 100vh;
display: flex !important; align-items: center; justify-content: center;
background: white; z-index: 99999; margin: 0; padding: 0;
}
#print-img { width: 100%; height: 100%; object-fit: contain; }
}
</style>
</head>
<body class="flex flex-col px-2 md:px-4"
onclick="if(window.resetIdleTimer) window.resetIdleTimer()"
ontouchstart="if(window.resetIdleTimer) window.resetIdleTimer()">
<div class="flex-none flex items-center justify-between gap-2 mb-1 bg-white/20 backdrop-blur-md p-1 rounded-3xl shadow-lg border-2 border-white/40 overflow-hidden relative z-50">
<div class="flex items-center gap-2 shrink-0 pl-1">
<button onclick="goHome()" class="bg-slate-900 text-white border-4 border-slate-700 hover:border-white px-3 py-2 rounded-full text-lg font-black shadow-xl flex items-center gap-2 transition-transform active:scale-95 no-underline focus:outline-none">
🏠 <span class="hidden lg:inline">MENÜ</span>
</button>
<h1 class="text-lg md:text-2xl font-black font-comic text-black tracking-widest drop-shadow-sm uppercase leading-none hidden md:block">
COMIC
</h1>
</div>
<div class="flex gap-1 overflow-x-auto no-scrollbar px-2 shrink-0 justify-center">
<button onclick="changeBg('#facc15')" class="w-10 h-10 rounded-xl bg-yellow-400 border-4 border-white hover:scale-110 transition shadow-md ring-2 ring-transparent active:ring-black"></button>
<button onclick="changeBg('#ef4444')" class="w-10 h-10 rounded-xl bg-red-500 border-4 border-white hover:scale-110 transition shadow-md ring-2 ring-transparent active:ring-black"></button>
<button onclick="changeBg('#3b82f6')" class="w-10 h-10 rounded-xl bg-blue-500 border-4 border-white hover:scale-110 transition shadow-md ring-2 ring-transparent active:ring-black"></button>
<button onclick="changeBg('#22c55e')" class="w-10 h-10 rounded-xl bg-green-500 border-4 border-white hover:scale-110 transition shadow-md ring-2 ring-transparent active:ring-black"></button>
<button onclick="changeBg('#a855f7')" class="w-10 h-10 rounded-xl bg-purple-500 border-4 border-white hover:scale-110 transition shadow-md ring-2 ring-transparent active:ring-black"></button>
<button onclick="changeBg('#ffffff')" class="w-10 h-10 rounded-xl bg-white border-4 border-gray-300 hover:scale-110 transition shadow-md ring-2 ring-transparent active:ring-black"></button>
</div>
<div class="flex items-center gap-2 shrink-0 pr-1">
<button onclick="switchCamera(); playSound('click')" class="w-12 h-12 bg-white hover:bg-gray-100 text-black rounded-full flex items-center justify-center text-xl shadow-lg border-4 border-slate-900 active:scale-95 transition" title="Kamera wechseln">
🔄
</button>
<div class="relative w-24 h-16 md:w-32 md:h-20 bg-black rounded-xl border-4 border-black shadow-xl overflow-hidden hidden sm:block">
<video id="webcam" autoplay playsinline muted class="w-full h-full object-cover opacity-90"></video>
<div class="absolute bottom-0 right-0 bg-red-600 text-white text-[8px] px-1 rounded-tl font-black animate-pulse pointer-events-none">LIVE</div>
</div>
<button onclick="toggleInfo(); playSound('click')" class="w-12 h-12 bg-slate-900 text-white rounded-full font-bold border-4 border-slate-700 hover:border-white shadow-xl flex items-center justify-center transition-transform active:scale-95 text-xl">
?
</button>
</div>
</div>
<div class="flex-1 flex flex-col md:flex-row gap-3 w-full max-w-[95rem] mx-auto mb-2 px-1 overflow-y-auto no-scrollbar z-10 relative min-h-0">
<div class="flex-1 bg-white border-[6px] border-black relative shadow-[4px_4px_0px_0px_rgba(0,0,0,0.8)] rounded-xl overflow-hidden group min-h-[150px]" id="panel-0">
<canvas class="w-full h-full object-cover bg-gray-100 pointer-events-none"></canvas>
<div class="absolute top-0 left-0 bg-black text-white px-3 py-0 text-2xl font-black font-comic rounded-br-lg pointer-events-none z-10">1</div>
<div class="bubble-container"><div class="bubble-handle"></div><div contenteditable="true" class="bubble-text" onfocus="clearText(this, 'Start...')" onblur="resetText(this, 'Start...')">Start...</div></div>
<button onclick="snap(0)" class="absolute bottom-2 right-2 bg-blue-600 hover:bg-blue-500 text-white w-16 h-16 rounded-full text-3xl shadow-xl border-4 border-white z-30 active:scale-90 transition flex items-center justify-center">📸</button>
</div>
<div class="flex-1 bg-white border-[6px] border-black relative shadow-[4px_4px_0px_0px_rgba(0,0,0,0.8)] rounded-xl overflow-hidden group min-h-[150px]" id="panel-1">
<canvas class="w-full h-full object-cover bg-gray-100 pointer-events-none"></canvas>
<div class="absolute top-0 left-0 bg-black text-white px-3 py-0 text-2xl font-black font-comic rounded-br-lg pointer-events-none z-10">2</div>
<div class="bubble-container"><div class="bubble-handle"></div><div contenteditable="true" class="bubble-text" onfocus="clearText(this, 'Dann...')" onblur="resetText(this, 'Dann...')">Dann...</div></div>
<button onclick="snap(1)" class="absolute bottom-2 right-2 bg-blue-600 hover:bg-blue-500 text-white w-16 h-16 rounded-full text-3xl shadow-xl border-4 border-white z-30 active:scale-90 transition flex items-center justify-center">📸</button>
</div>
<div class="flex-1 bg-white border-[6px] border-black relative shadow-[4px_4px_0px_0px_rgba(0,0,0,0.8)] rounded-xl overflow-hidden group min-h-[150px]" id="panel-2">
<canvas class="w-full h-full object-cover bg-gray-100 pointer-events-none"></canvas>
<div class="absolute top-0 left-0 bg-black text-white px-3 py-0 text-2xl font-black font-comic rounded-br-lg pointer-events-none z-10">3</div>
<div class="bubble-container"><div class="bubble-handle"></div><div contenteditable="true" class="bubble-text" onfocus="clearText(this, 'Ende!')" onblur="resetText(this, 'Ende!')">Ende!</div></div>
<button onclick="snap(2)" class="absolute bottom-2 right-2 bg-blue-600 hover:bg-blue-500 text-white w-16 h-16 rounded-full text-3xl shadow-xl border-4 border-white z-30 active:scale-90 transition flex items-center justify-center">📸</button>
</div>
</div>
<div class="text-center flex justify-center gap-4 px-4 z-20 relative shrink-0">
<button onclick="playSound('click'); downloadComic()" class="flex-1 max-w-xs bg-slate-900 text-white font-black font-comic text-xl md:text-2xl py-3 rounded-2xl border-b-[6px] border-slate-700 hover:border-slate-500 hover:translate-y-[-2px] active:border-b-0 active:translate-y-2 transition-all shadow-xl flex items-center justify-center gap-2">💾 SPEICHERN</button>
<button id="btn-print" onclick="playSound('click'); window.printComic()" class="flex-1 max-w-xs bg-white text-black font-black font-comic text-xl md:text-2xl py-3 rounded-2xl border-b-[6px] border-gray-300 hover:border-gray-400 hover:translate-y-[-2px] active:border-b-0 active:translate-y-2 transition-all shadow-xl flex items-center justify-center gap-2">🖨️ DRUCKEN</button>
</div>
<div id="info-modal" class="hidden fixed inset-0 z-[200] bg-black/80 backdrop-blur-sm flex items-center justify-center p-4" onclick="toggleInfo()">
<div class="bg-slate-800 border-4 border-yellow-400 rounded-[2rem] max-w-3xl w-full p-8 shadow-2xl relative" onclick="event.stopPropagation()">
<button onclick="toggleInfo(); playSound('click')" class="absolute top-6 right-6 text-white hover:text-yellow-400 text-4xl font-bold transition"></button>
<h2 class="text-4xl font-black text-white mb-8 border-b border-slate-600 pb-4 flex items-center gap-4"><span class="text-6xl">📸</span> ANLEITUNG</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 text-base">
<div class="p-6 bg-slate-700/50 rounded-2xl border border-slate-600 flex flex-col items-center text-center"><div class="text-4xl mb-4 text-blue-400">📸</div><h3 class="text-white font-bold text-xl mb-2">Fotografieren</h3><p class="text-slate-300 leading-snug">Drücke auf den blauen Kamera-Button in jedem Bildrahmen.</p></div>
<div class="p-6 bg-slate-700/50 rounded-2xl border border-slate-600 flex flex-col items-center text-center"><div class="text-4xl mb-4 text-white">💬</div><h3 class="text-white font-bold text-xl mb-2">Texten</h3><p class="text-slate-300 leading-snug">Tippe auf den Text. Du kannst die Blase am blauen Griff verschieben.</p></div>
<div class="p-6 bg-slate-700/50 rounded-2xl border border-slate-600 flex flex-col items-center text-center"><div class="text-4xl mb-4 text-yellow-400">🎨</div><h3 class="text-white font-bold text-xl mb-2">Hintergrund</h3><p class="text-slate-300 leading-snug">Wähle oben eine Farbe für deinen Comic aus!</p></div>
</div>
<button onclick="toggleInfo(); playSound('click')" class="mt-8 w-full bg-yellow-500 hover:bg-yellow-400 text-black font-black py-4 rounded-xl text-xl transition">ALLES KLAR!</button>
</div>
</div>
<div id="print-container" class="hidden"><img id="print-img" src=""></div>
<script>
const _soundCache = { 'click': new Audio('../assets/sounds/click.mp3'), 'shutter': new Audio('../assets/sounds/shutter.mp3'), 'success': new Audio('../assets/sounds/success.mp3') };
Object.values(_soundCache).forEach(s => s.load());
window.playSound = function(id) { if (_soundCache[id]) { const s = _soundCache[id].cloneNode(); s.volume = 1.0; s.currentTime = 0; s.play().catch(e => console.warn(e)); } };
window.goHome = function() { playSound('click'); setTimeout(() => { window.location.href = '../index.html'; }, 300); };
const video = document.getElementById('webcam'); let currentFacingMode = 'user'; let currentStream; let idleTimer; let currentBgColor = '#facc15';
function resetIdleTimer() { clearTimeout(idleTimer); idleTimer = setTimeout(() => { window.location.href = '../index.html'; }, 120000); }
['mousedown', 'touchstart', 'scroll', 'keydown', 'input'].forEach(evt => document.addEventListener(evt, resetIdleTimer, {passive: true})); resetIdleTimer();
function toggleInfo() { const modal = document.getElementById('info-modal'); if (modal.classList.contains('hidden')) { modal.classList.remove('hidden'); modal.classList.add('flex'); } else { modal.classList.add('hidden'); modal.classList.remove('flex'); } }
async function startCamera() { try { if (currentStream) { currentStream.getTracks().forEach(track => track.stop()); } const stream = await navigator.mediaDevices.getUserMedia({ video: { width: { ideal: 1280 }, height: { ideal: 720 }, facingMode: currentFacingMode } }); currentStream = stream; video.srcObject = stream; } catch (err) { alert("Kamera Fehler: " + err.message); } }
function switchCamera() { currentFacingMode = (currentFacingMode === 'user') ? 'environment' : 'user'; startCamera(); }
function snap(panelIndex) { playSound('shutter'); const panel = document.getElementById('panel-' + panelIndex); const canvas = panel.querySelector('canvas'); const ctx = canvas.getContext('2d'); canvas.width = 1280; canvas.height = 720; const vRatio = video.videoWidth / video.videoHeight; const cRatio = canvas.width / canvas.height; let sx, sy, sWidth, sHeight; if (vRatio > cRatio) { sHeight = video.videoHeight; sWidth = sHeight * cRatio; sx = (video.videoWidth - sWidth) / 2; sy = 0; } else { sWidth = video.videoWidth; sHeight = sWidth / cRatio; sx = 0; sy = (video.videoHeight - sHeight) / 2; } ctx.save(); if(currentFacingMode === 'user') { ctx.scale(-1, 1); ctx.drawImage(video, sx, sy, sWidth, sHeight, -canvas.width, 0, canvas.width, canvas.height); } else { ctx.drawImage(video, sx, sy, sWidth, sHeight, 0, 0, canvas.width, canvas.height); } ctx.restore(); panel.style.borderColor = '#3b82f6'; setTimeout(() => panel.style.borderColor = 'black', 200); }
function changeBg(color) { playSound('click'); currentBgColor = color; document.body.style.backgroundColor = color; }
function clearText(el, defaultText) { if (el.innerText.trim() === defaultText) el.innerText = ""; }
function resetText(el, defaultText) { if (el.innerText.trim() === "") el.innerText = defaultText; }
const handles = document.querySelectorAll('.bubble-handle'); let activeDrag = null; let startX, startY, initialLeft, initialTop;
handles.forEach(handle => { handle.addEventListener('mousedown', startDrag); handle.addEventListener('touchstart', startDrag, {passive: false}); });
function startDrag(e) { e.preventDefault(); activeDrag = e.target.parentElement; const clientX = e.clientX || e.touches[0].clientX; const clientY = e.clientY || e.touches[0].clientY; startX = clientX; startY = clientY; initialLeft = activeDrag.offsetLeft; initialTop = activeDrag.offsetTop; document.addEventListener('mousemove', doDrag); document.addEventListener('mouseup', stopDrag); document.addEventListener('touchmove', doDrag, {passive: false}); document.addEventListener('touchend', stopDrag); }
function doDrag(e) { if (!activeDrag) return; e.preventDefault(); const clientX = e.clientX || e.touches[0].clientX; const clientY = e.clientY || e.touches[0].clientY; const dx = clientX - startX; const dy = clientY - startY; activeDrag.style.left = (initialLeft + dx) + 'px'; activeDrag.style.top = (initialTop + dy) + 'px'; activeDrag.style.transform = 'none'; }
function stopDrag() { activeDrag = null; document.removeEventListener('mousemove', doDrag); document.removeEventListener('mouseup', stopDrag); document.removeEventListener('touchmove', doDrag); document.removeEventListener('touchend', stopDrag); }
function generateComicImage() { const refPanel = document.getElementById('panel-0'); const refRect = refPanel.getBoundingClientRect(); const screenRatio = refRect.width / refRect.height; const panelW = 1280; const panelH = panelW / screenRatio; const border = 60; const gap = 40; const titleSpace = 250; const master = document.createElement('canvas'); master.width = (panelW * 3) + (gap * 2) + (border * 2); master.height = panelH + (border * 2) + titleSpace; const ctx = master.getContext('2d'); ctx.fillStyle = currentBgColor; ctx.fillRect(0, 0, master.width, master.height); ctx.fillStyle = 'black'; ctx.font = '900 120px "Comic Sans MS", sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText("MEINE FOTO-STORY", master.width / 2, titleSpace / 2); [0, 1, 2].forEach(i => { const panelDiv = document.getElementById('panel-' + i); const srcCanvas = panelDiv.querySelector('canvas'); const textDiv = panelDiv.querySelector('.bubble-text'); const panelOffsetX = border + (i * (panelW + gap)); const panelOffsetY = titleSpace; ctx.fillStyle = '#eee'; ctx.fillRect(panelOffsetX, panelOffsetY, panelW, panelH); if (srcCanvas.width > 0) { const srcW = srcCanvas.width; const srcH = srcCanvas.height; const srcRatio = srcW / srcH; let sW, sH, sX, sY; if (srcRatio > screenRatio) { sH = srcH; sW = srcH * screenRatio; sX = (srcW - sW) / 2; sY = 0; } else { sW = srcW; sH = srcW / screenRatio; sX = 0; sY = (srcH - sH) / 2; } ctx.drawImage(srcCanvas, sX, sY, sW, sH, panelOffsetX, panelOffsetY, panelW, panelH); } ctx.lineWidth = 20; ctx.strokeStyle = 'black'; ctx.strokeRect(panelOffsetX, panelOffsetY, panelW, panelH); ctx.fillStyle = 'black'; ctx.fillRect(panelOffsetX, panelOffsetY, 120, 120); ctx.fillStyle = 'white'; ctx.font = 'bold 80px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(i + 1, panelOffsetX + 60, panelOffsetY + 65); const text = textDiv.innerText.trim(); if (text) { const domPanelRect = panelDiv.getBoundingClientRect(); const domBubbleRect = textDiv.getBoundingClientRect(); const scaleX = panelW / domPanelRect.width; const scaleY = panelH / domPanelRect.height; const relX = domBubbleRect.left - domPanelRect.left; const relY = domBubbleRect.top - domPanelRect.top; const bubbleX = panelOffsetX + (relX * scaleX); const bubbleY = panelOffsetY + (relY * scaleY); drawRoundedRectBubble(ctx, bubbleX, bubbleY, text); } }); return master.toDataURL('image/png'); }
function drawRoundedRectBubble(ctx, x, y, text) { ctx.font = 'bold 50px "Comic Sans MS", sans-serif'; const padding = 50; const lineHeight = 60; const maxWidth = 500; const words = text.split(' '); let line = ''; let lines = []; for(let n = 0; n < words.length; n++) { let testLine = line + words[n] + ' '; let metrics = ctx.measureText(testLine); if (metrics.width > maxWidth && n > 0) { lines.push(line); line = words[n] + ' '; } else { line = testLine; } } lines.push(line); let maxLineWidth = 0; lines.forEach(l => { const m = ctx.measureText(l); if(m.width > maxLineWidth) maxLineWidth = m.width; }); const boxW = maxLineWidth + (padding * 2); const boxH = (lines.length * lineHeight) + (padding * 2); ctx.fillStyle = 'rgba(0,0,0,0.2)'; roundRect(ctx, x + 15, y + 15, boxW, boxH, 40, true, false); ctx.fillStyle = 'white'; ctx.strokeStyle = 'black'; ctx.lineWidth = 10; roundRect(ctx, x, y, boxW, boxH, 40, true, true); ctx.fillStyle = 'black'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; let ty = y + padding + (lineHeight/2); const centerX = x + (boxW/2); lines.forEach(l => { ctx.fillText(l.trim(), centerX, ty); ty += lineHeight; }); }
function roundRect(ctx, x, y, w, h, r, fill, stroke) { ctx.beginPath(); ctx.moveTo(x+r, y); ctx.arcTo(x+w, y, x+w, y+h, r); ctx.arcTo(x+w, y+h, x, y+h, r); ctx.arcTo(x, y+h, x, y, r); ctx.arcTo(x, y, x+w, y, r); ctx.closePath(); if (fill) ctx.fill(); if (stroke) ctx.stroke(); }
function downloadComic() { playSound('success'); const dataUrl = generateComicImage(); const link = document.createElement('a'); link.download = 'mein-comic.png'; link.href = dataUrl; link.click(); }
window.printComic = async function() { const btn = document.getElementById('btn-print'); const oldText = btn ? btn.innerText : "🖨️ DRUCKEN"; if(btn) { btn.innerText = "⏳ Sende..."; btn.disabled = true; } const dataUrl = generateComicImage(); try { if (window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.DirectPrinter) { await window.Capacitor.Plugins.DirectPrinter.printBase64({ data: dataUrl, name: 'Comic-Studio' }); if(btn) btn.innerText = "✅ Gedruckt!"; playSound('success'); } else { console.warn("DirectPrinter Plugin nicht gefunden, nutze Browser Print."); const printImg = document.getElementById('print-img'); printImg.src = dataUrl; printImg.onload = () => { setTimeout(() => { window.print(); }, 500); }; if(btn) btn.innerText = "✅ OK"; playSound('success'); } } catch (error) { console.error("Druckfehler:", error); alert("Drucken fehlgeschlagen: " + error.message); if(btn) btn.innerText = "❌ Fehler"; } setTimeout(() => { if(btn) { btn.innerText = oldText; btn.disabled = false; } }, 3000); }
window.onload = startCamera;
</script>
</body>
</html>

137
public/apps/gif.html Normal file
View File

@@ -0,0 +1,137 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Loop Cam</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<script src="../js/tailwind.js"></script>
<style>
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
html { width: 100%; height: 100%; background-color: #0f172a; }
body {
position: fixed; top: 0; left: 0; right: 0; bottom: 0; margin: 0;
background-color: #0f172a;
color: white;
overflow: hidden !important;
user-select: none;
touch-action: none;
overscroll-behavior: none;
font-family: sans-serif;
animation: fadeIn 0.4s ease-out;
box-sizing: border-box;
padding-top: calc(10px + env(safe-area-inset-top));
padding-bottom: max(20px, env(safe-area-inset-bottom));
}
video, canvas { display: block; }
.no-scrollbar::-webkit-scrollbar { display: none; }
button:active { transform: scale(0.95); transition: transform 0.1s; }
</style>
</head>
<body class="flex flex-col p-2 md:p-4"
onclick="if(window.resetIdleTimer) window.resetIdleTimer()"
ontouchstart="if(window.resetIdleTimer) window.resetIdleTimer()">
<div class="flex-none flex justify-between items-center z-50 mb-2 relative h-16">
<button onclick="goHome()" class="bg-slate-800 text-white border-4 border-slate-600 hover:border-white px-4 py-2 rounded-full text-lg font-black shadow-xl flex items-center gap-2 transition-transform active:scale-95 no-underline focus:outline-none shrink-0 z-50">
🏠 MENÜ
</button>
<div class="absolute inset-0 flex items-center justify-center pointer-events-none z-0">
<h1 class="text-2xl md:text-4xl font-black text-white drop-shadow-[0_4px_4px_rgba(0,0,0,0.8)] tracking-widest uppercase">
✨ LOOP CAM
</h1>
</div>
<button onclick="toggleInfo(); playSound('click')" class="w-12 h-12 bg-slate-800 text-white rounded-full font-bold border-4 border-slate-600 hover:border-white shadow-xl flex items-center justify-center transition-transform active:scale-95 text-xl shrink-0 z-50">
?
</button>
</div>
<div class="flex-1 flex flex-row gap-2 md:gap-4 min-h-0 items-center justify-center w-full max-w-[95rem] mx-auto overflow-hidden pb-1">
<div class="w-20 md:w-24 flex flex-col gap-2 bg-gray-900/50 p-2 rounded-2xl border border-white/10 shadow-2xl shrink-0 h-full max-h-full overflow-hidden">
<div class="text-[8px] font-bold text-gray-500 uppercase tracking-widest text-center w-full py-1 shrink-0">Filter</div>
<div class="flex-1 overflow-y-auto no-scrollbar flex flex-col items-center justify-evenly w-full pb-1">
<button onclick="setFilter('')" class="w-16 h-12 rounded-xl bg-white text-black font-black text-[10px] hover:scale-105 active:scale-95 transition border-2 border-transparent flex flex-col items-center justify-center"><span></span> NORM</button>
<button onclick="setFilter('grayscale(100%) contrast(1.2)')" class="w-16 h-12 rounded-xl bg-gray-600 text-white font-black text-[10px] hover:scale-105 active:scale-95 transition border-2 border-white/20 flex flex-col items-center justify-center" style="filter: grayscale(100%)"><span></span> S/W</button>
<button onclick="setFilter('sepia(80%)')" class="w-16 h-12 rounded-xl bg-amber-700 text-white font-black text-[10px] hover:scale-105 active:scale-95 transition border-2 border-white/20 flex flex-col items-center justify-center" style="filter: sepia(80%)"><span>📜</span> ALT</button>
<button onclick="setFilter('invert(100%)')" class="w-16 h-12 rounded-xl bg-blue-600 text-white font-black text-[10px] hover:scale-105 active:scale-95 transition border-2 border-white/20 flex flex-col items-center justify-center" style="filter: invert(100%)"><span>💀</span> XRAY</button>
<button onclick="setFilter('saturate(250%)')" class="w-16 h-12 rounded-xl bg-pink-500 text-white font-black text-[10px] hover:scale-105 active:scale-95 transition border-2 border-white/20 flex flex-col items-center justify-center" style="filter: saturate(250%)"><span>🎨</span> POP</button>
<button onclick="setFilter('hue-rotate(90deg)')" class="w-16 h-12 rounded-xl bg-emerald-500 text-white font-black text-[10px] hover:scale-105 active:scale-95 transition border-2 border-white/20 flex flex-col items-center justify-center" style="filter: hue-rotate(90deg)"><span>👽</span> ALIEN</button>
</div>
</div>
<div class="flex-1 flex flex-col items-center h-full min-w-0 gap-2 md:gap-4 overflow-hidden">
<div id="video-container" class="relative flex-1 w-full bg-gray-900 rounded-[1.5rem] overflow-hidden border-[6px] border-gray-800 shadow-2xl group transition-colors duration-200 flex items-center justify-center min-h-0">
<video id="video" autoplay playsinline muted class="w-full h-full object-contain"></video>
<canvas id="canvas" class="w-full h-full object-contain hidden"></canvas>
<div id="countdown" class="absolute inset-0 flex items-center justify-center text-[8rem] font-black text-white hidden bg-black/50 backdrop-blur-sm z-30">3</div>
<div id="rec-indicator" class="absolute top-4 right-4 hidden items-center gap-2 z-30 bg-red-600 text-white px-3 py-1 rounded-full font-black text-sm animate-pulse shadow-lg"><div class="w-2 h-2 bg-white rounded-full"></div> REC</div>
<div id="saving-overlay" class="absolute inset-0 bg-black/80 z-50 hidden flex-col items-center justify-center text-white">
<div class="text-6xl animate-spin mb-4">💾</div>
<div class="text-2xl font-bold">Speichere...</div>
</div>
</div>
<div class="h-16 md:h-20 w-full flex-shrink-0 flex items-center justify-center relative">
<div id="start-overlay" class="absolute w-full flex justify-center items-center gap-4 z-20 pointer-events-none">
<button onclick="switchCamera(); playSound('click')" id="switch-btn" class="pointer-events-auto w-14 h-14 bg-slate-700 hover:bg-slate-600 text-white rounded-xl shadow-xl border-b-4 border-slate-900 active:border-b-0 active:translate-y-2 transition-all flex items-center justify-center shrink-0" title="Kamera wechseln"><span class="text-2xl">🔄</span></button>
<button onclick="playSound('click'); startCountdown()" class="pointer-events-auto bg-sky-500 hover:bg-sky-400 text-white text-2xl font-black py-3 px-8 rounded-xl shadow-2xl border-b-4 border-sky-700 active:border-b-0 active:translate-y-2 transition-all w-full max-w-xs flex items-center justify-center gap-2"><span>🎥</span> ACTION!</button>
</div>
<div id="action-buttons" class="hidden gap-2 md:gap-4 w-full justify-center z-20 pointer-events-none">
<button onclick="reset(); playSound('click')" class="pointer-events-auto bg-yellow-500 hover:bg-yellow-400 text-black text-xl font-black py-3 px-6 rounded-xl border-b-4 border-yellow-700 active:border-b-0 active:translate-y-2 transition-all shadow-xl flex items-center justify-center gap-2 flex-1 max-w-[150px]">🔄 NOCHMAL</button>
<button onclick="saveLoopVideo(); playSound('success')" class="pointer-events-auto bg-green-600 hover:bg-green-500 text-white text-xl font-black py-3 px-6 rounded-xl border-b-4 border-green-800 active:border-b-0 active:translate-y-2 transition-all shadow-xl flex items-center justify-center gap-2 flex-1 max-w-[150px]">💾 SPEICHERN</button>
</div>
</div>
</div>
<div class="w-20 md:w-24 flex-shrink-0 flex flex-col justify-center gap-2 z-40 bg-gray-900/50 p-2 rounded-2xl border border-white/10 shadow-2xl h-full max-h-full overflow-hidden transition-opacity opacity-30 pointer-events-none" id="speed-controls">
<div class="text-center text-[8px] font-bold text-gray-400 mb-1 uppercase tracking-widest shrink-0">Tempo</div>
<div class="flex-1 flex flex-col justify-center gap-2">
<button id="btn-slow" onclick="setSpeed(150)" class="w-full h-16 bg-green-700 hover:bg-green-600 text-white rounded-lg shadow-lg border-b-2 border-green-900 transition active:scale-95 flex flex-col items-center justify-center gap-1 opacity-50"><span class="text-xl">🐢</span><span class="text-[8px] font-black uppercase">Langsam</span></button>
<button id="btn-norm" onclick="setSpeed(40)" class="w-full h-16 bg-blue-700 hover:bg-blue-600 text-white rounded-lg shadow-lg border-b-2 border-blue-900 transition active:scale-95 flex flex-col items-center justify-center gap-1 opacity-50"><span class="text-xl">🚶</span><span class="text-[8px] font-black uppercase">Mittel</span></button>
<button id="btn-turbo" onclick="setSpeed(10)" class="w-full h-16 bg-red-700 hover:bg-red-600 text-white rounded-lg shadow-lg border-b-2 border-red-900 transition active:scale-95 flex flex-col items-center justify-center gap-1 opacity-50"><span class="text-xl">🚀</span><span class="text-[8px] font-black uppercase">Turbo</span></button>
</div>
</div>
</div>
<div id="info-modal" class="hidden fixed inset-0 z-[200] bg-black/80 backdrop-blur-sm items-center justify-center p-4" onclick="toggleInfo()">
<div class="bg-slate-800 border-4 border-sky-500 rounded-[2rem] max-w-2xl w-full p-8 shadow-2xl relative" onclick="event.stopPropagation()">
<button onclick="toggleInfo(); playSound('click')" class="absolute top-6 right-6 text-white hover:text-sky-400 text-4xl font-bold transition"></button>
<h2 class="text-4xl font-black text-white mb-8 border-b border-slate-600 pb-4 flex items-center gap-4"><span class="text-6xl">📹</span> ANLEITUNG</h2>
<div class="text-lg text-slate-300 space-y-4">
<p>1. Wähle links einen <b>Filter</b>.</p>
<p>2. Drücke unten auf <b>ACTION!</b>.</p>
<p>3. Nach der Aufnahme kannst du rechts das <b>Tempo</b> ändern.</p>
<p>4. Mit <b>🔄</b> wechselst du die Kamera.</p>
</div>
<button onclick="toggleInfo(); playSound('click')" class="mt-8 w-full bg-sky-600 hover:bg-sky-500 text-white font-black py-4 rounded-xl text-xl transition">ALLES KLAR!</button>
</div>
</div>
<script>
const _soundCache = {'click': new Audio('../assets/sounds/click.mp3'), 'shutter': new Audio('../assets/sounds/shutter.mp3'), 'success': new Audio('../assets/sounds/success.mp3')};
Object.values(_soundCache).forEach(s => s.load());
window.playSound = function(id) { if(_soundCache[id]) { const s = _soundCache[id].cloneNode(); s.volume = 1.0; s.play().catch(e=>{}); } };
window.goHome = function() { playSound('click'); setTimeout(() => { window.location.href = '../index.html'; }, 300); };
let idleTimer;
window.resetIdleTimer = function() { clearTimeout(idleTimer); idleTimer = setTimeout(() => { if (document.getElementById('countdown').classList.contains('hidden') && document.getElementById('saving-overlay').classList.contains('hidden')) { window.location.href = '../index.html'; } else { window.resetIdleTimer(); } }, 120000); };
['mousedown', 'touchstart', 'scroll', 'keydown', 'input'].forEach(evt => document.addEventListener(evt, window.resetIdleTimer, {passive: true})); window.resetIdleTimer();
window.toggleInfo = function() { const modal = document.getElementById('info-modal'); modal.classList.toggle('hidden'); modal.classList.toggle('flex'); }
let video, canvas, ctx, videoContainer, recIndicator, startBtn, resultBtns, speedWrapper, switchBtn; let frames = []; let speed = 40; let loopInterval; let currentStream; let currentFacingMode = 'user';
window.initCam = async function() { video = document.getElementById('video'); canvas = document.getElementById('canvas'); ctx = canvas.getContext('2d'); videoContainer = document.getElementById('video-container'); recIndicator = document.getElementById('rec-indicator'); startBtn = document.getElementById('start-overlay'); resultBtns = document.getElementById('action-buttons'); speedWrapper = document.getElementById('speed-controls'); switchBtn = document.getElementById('switch-btn'); if(!video) return; try { if (currentStream && typeof currentStream.getTracks === 'function') currentStream.getTracks().forEach(track => track.stop()); const stream = await navigator.mediaDevices.getUserMedia({ video: { width: {ideal: 1280}, height: {ideal: 720}, facingMode: currentFacingMode } }); currentStream = stream; video.srcObject = stream; video.onloadedmetadata = () => { video.play(); }; video.style.transform = (currentFacingMode === 'user') ? 'scaleX(-1)' : 'scaleX(1)'; } catch (err) { alert("Kamera-Fehler: " + err.message); } }
window.switchCamera = function() { currentFacingMode = (currentFacingMode === 'user') ? 'environment' : 'user'; window.initCam(); }
window.setFilter = function(filter) { playSound('click'); if(video) video.style.filter = filter; if(canvas) canvas.style.filter = filter; }
window.startCountdown = function() { startBtn.classList.add('hidden'); switchBtn.classList.add('hidden'); const cd = document.getElementById('countdown'); cd.classList.remove('hidden'); cd.style.display = 'flex'; let count = 3; cd.innerText = count; const timer = setInterval(() => { count--; if (count > 0) { cd.innerText = count; } else { clearInterval(timer); cd.style.display = 'none'; playSound('shutter'); record(); } }, 800); }
function record() { frames = []; canvas.width = video.videoWidth; canvas.height = video.videoHeight; videoContainer.classList.replace('border-gray-800', 'border-red-600'); recIndicator.classList.remove('hidden'); recIndicator.style.display = 'flex'; let startTime = Date.now(); function captureLoop() { if (Date.now() - startTime < 2000) { ctx.save(); if (currentFacingMode === 'user') { ctx.translate(canvas.width, 0); ctx.scale(-1, 1); } ctx.drawImage(video, 0, 0, canvas.width, canvas.height); ctx.restore(); frames.push(ctx.getImageData(0, 0, canvas.width, canvas.height)); requestAnimationFrame(captureLoop); } else { play(); } } captureLoop(); }
function play() { videoContainer.classList.replace('border-red-600', 'border-gray-800'); recIndicator.classList.add('hidden'); recIndicator.style.display = ''; video.classList.add('hidden'); canvas.classList.remove('hidden'); resultBtns.classList.remove('hidden'); resultBtns.style.display = 'flex'; speedWrapper.classList.remove('opacity-30', 'pointer-events-none'); window.setSpeed(40); const loopFrames = [...frames, ...[...frames].reverse()]; startLoop(loopFrames, 0); }
function startLoop(loopFrames, i) { clearInterval(loopInterval); loopInterval = setInterval(() => { ctx.putImageData(loopFrames[i], 0, 0); i = (i + 1) % loopFrames.length; }, speed); }
window.setSpeed = function(ms) { playSound('click'); speed = ms; updateSpeedUI(); if (canvas && !canvas.classList.contains('hidden')) { const loopFrames = [...frames, ...[...frames].reverse()]; startLoop(loopFrames, 0); } }
function updateSpeedUI() { const btns = { 150: document.getElementById('btn-slow'), 40: document.getElementById('btn-norm'), 10: document.getElementById('btn-turbo') }; Object.values(btns).forEach(btn => { if(btn) { btn.classList.remove('opacity-100', 'scale-105', 'border-white'); btn.classList.add('opacity-50', 'border-transparent'); } }); const active = btns[speed]; if(active) { active.classList.remove('opacity-50', 'border-transparent'); active.classList.add('opacity-100', 'scale-105', 'border-white'); } }
window.saveLoopVideo = function() { document.getElementById('saving-overlay').classList.remove('hidden'); document.getElementById('saving-overlay').style.display = 'flex'; const stream = canvas.captureStream(30); const recorder = new MediaRecorder(stream, { mimeType: 'video/webm' }); const chunks = []; recorder.ondataavailable = e => chunks.push(e.data); recorder.onstop = () => { const blob = new Blob(chunks, { type: 'video/webm' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'mein-loop.webm'; a.click(); document.getElementById('saving-overlay').classList.add('hidden'); document.getElementById('saving-overlay').style.display = ''; }; recorder.start(); setTimeout(() => recorder.stop(), 3000); }
window.reset = function() { clearInterval(loopInterval); video.classList.remove('hidden'); canvas.classList.add('hidden'); startBtn.classList.remove('hidden'); resultBtns.classList.add('hidden'); resultBtns.style.display = 'none'; speedWrapper.classList.add('opacity-30', 'pointer-events-none'); switchBtn.classList.remove('hidden'); window.setSpeed(40); videoContainer.classList.replace('border-red-600', 'border-gray-800'); recIndicator.classList.add('hidden'); recIndicator.style.display = ''; }
window.addEventListener('beforeunload', () => { try { if(currentStream) currentStream.getTracks().forEach(track => track.stop()); } catch(e) {} });
window.onload = window.initCam;
</script>
</body>
</html>

276
public/apps/magic.html Normal file
View File

@@ -0,0 +1,276 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Magic Selfie</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<script src="../js/tailwind.js"></script>
<script src="../js/camera_utils.js"></script>
<script src="../js/selfie_segmentation.js"></script>
<style>
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
html {
width: 100%; height: 100%;
background-color: #0f172a;
}
body {
/* Fixiertes Layout */
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
margin: 0;
background-color: #0f172a;
color: white;
overflow: hidden !important;
user-select: none;
touch-action: none;
transition: background-color 0.5s;
animation: fadeIn 0.4s ease-out;
box-sizing: border-box;
/* Body Padding */
padding-top: calc(10px + env(safe-area-inset-top));
padding-bottom: max(20px, env(safe-area-inset-bottom));
}
.no-scrollbar::-webkit-scrollbar { display: none; }
button:active { transform: scale(0.95); transition: transform 0.1s; }
@media print {
body * { visibility: hidden; }
#print-container, #print-container * { visibility: visible; }
#print-container { position: fixed; left: 0; top: 0; width: 100%; height: 100%; display: flex !important; align-items: center; justify-content: center; background: white; z-index: 99999; }
#print-img { max-width: 100%; max-height: 100%; object-fit: contain; }
}
</style>
</head>
<body class="flex flex-col md:flex-row bg-slate-900" onclick="window.resetIdleTimer()" ontouchstart="window.resetIdleTimer()">
<button onclick="goHome()" class="absolute left-6 z-[100] bg-slate-800 text-white border-4 border-slate-600 hover:border-white px-6 py-3 rounded-full text-xl font-black shadow-2xl flex items-center gap-3 transition-transform active:scale-95 no-underline" style="top: calc(20px + env(safe-area-inset-top));">
🏠 MENÜ
</button>
<div class="absolute w-full text-center pointer-events-none z-50 hidden md:block" style="top: calc(20px + env(safe-area-inset-top));">
<h1 class="text-3xl font-black text-white drop-shadow-[0_4px_4px_rgba(0,0,0,0.8)] tracking-widest uppercase">
✨ MAGIC SELFIE
</h1>
</div>
<button onclick="window.toggleInfo(); playSound('click')" class="absolute right-6 z-[100] w-14 h-14 bg-slate-800 text-white rounded-full font-bold border-4 border-slate-600 hover:border-white shadow-xl flex items-center justify-center transition-transform active:scale-95 text-2xl" style="top: calc(20px + env(safe-area-inset-top));">
?
</button>
<div class="relative flex-1 bg-black flex flex-col items-center justify-center overflow-hidden h-[60%] md:h-full w-full rounded-b-3xl md:rounded-r-3xl md:rounded-bl-none shadow-2xl z-10">
<div class="relative w-full h-full flex items-center justify-center p-2 md:p-8 pb-24">
<video id="ai-video" autoplay playsinline muted class="hidden"></video>
<canvas id="ai-canvas" class="max-w-full max-h-full object-contain shadow-2xl rounded-lg"></canvas>
<div id="preview-overlay" class="absolute inset-0 bg-slate-900 z-50 hidden flex-col items-center justify-center">
<h2 class="text-3xl font-bold text-white mb-6 animate-bounce">Dein magisches Foto!</h2>
<div class="relative max-w-[90%] max-h-[60%] border-8 border-white shadow-2xl rounded-xl overflow-hidden bg-black">
<img id="preview-img-display" class="w-full h-full object-contain" src="">
</div>
<div class="flex gap-6 mt-8">
<button onclick="window.closePreview(); playSound('click')" class="bg-yellow-500 hover:bg-yellow-400 text-black font-black py-4 px-8 rounded-2xl text-xl shadow-xl active:scale-95 transition border-b-8 border-yellow-700 active:border-b-0 active:translate-y-2 flex items-center gap-2">🔄 NOCHMAL</button>
<button id="btn-print" onclick="window.printFromPreview(this)" class="bg-white text-slate-900 font-black py-4 px-8 rounded-2xl text-xl shadow-xl hover:bg-gray-100 active:scale-95 transition border-b-8 border-gray-400 active:border-b-0 active:translate-y-2 flex items-center gap-2">🖨️ DRUCKEN</button>
<button onclick="window.saveFromPreview()" class="bg-violet-600 text-white font-black py-4 px-8 rounded-2xl text-xl shadow-xl hover:bg-violet-500 active:scale-95 transition border-b-8 border-violet-800 active:border-b-0 active:translate-y-2 flex items-center gap-2">💾 FOTO</button>
</div>
</div>
</div>
<div id="ai-loading" class="absolute inset-0 bg-black/95 flex flex-col items-center justify-center z-40 p-4 text-center">
<div class="text-6xl animate-spin mb-4">🔮</div>
<div class="text-2xl font-bold mb-4">Lade KI...</div>
<div id="ai-debug-box" class="text-xs text-left font-mono bg-black/80 border border-gray-700 p-4 rounded-xl mt-4 max-w-lg w-full h-32 overflow-y-auto opacity-70"><div>[Log] App gestartet...</div></div>
<div class="mt-8 flex gap-6">
<button onclick="location.reload()" class="border-2 border-white/30 px-6 py-3 rounded-full hover:bg-white/10">Neu laden</button>
<button onclick="window.drawFallback(); document.getElementById('ai-loading').style.display='none'; playSound('click')" class="bg-red-600 px-6 py-3 rounded-full font-bold hover:bg-red-500">Ohne KI starten</button>
</div>
</div>
<div id="main-controls" class="absolute bottom-6 flex gap-4 z-30 w-full justify-center px-4">
<button onclick="window.switchCamera(); playSound('click')" class="w-20 bg-slate-700 text-white font-black py-4 rounded-2xl text-2xl shadow-xl hover:scale-105 active:scale-95 transition border-b-8 border-slate-900 active:border-b-0 active:translate-y-2 flex items-center justify-center" title="Kamera wechseln">🔄</button>
<button onclick="window.capturePhoto()" class="flex-1 max-w-[200px] bg-violet-600 text-white font-black py-4 rounded-2xl text-lg md:text-xl shadow-xl hover:scale-105 active:scale-95 transition border-b-8 border-violet-800 active:border-b-0 active:translate-y-2 flex items-center justify-center gap-2">✨ FOTO</button>
</div>
</div>
<div class="h-[40%] md:h-full md:w-1/3 min-w-[280px] max-w-md bg-slate-800 p-4 border-t-4 md:border-t-0 md:border-l-4 border-slate-700 overflow-y-auto no-scrollbar z-20 flex flex-col">
<h3 class="text-white font-black text-xl mb-4 text-center uppercase tracking-wider sticky top-0 bg-slate-800 pb-2 z-10 pt-2">🌍 Reiseziel</h3>
<div class="grid grid-cols-2 gap-3 pb-4">
<div onclick="window.setBg('../assets/weltraum.jpg'); playSound('click')" class="aspect-square bg-slate-700 rounded-2xl cursor-pointer border-4 border-transparent hover:border-violet-400 relative group overflow-hidden active:scale-95 transition shadow-lg"><img src="../assets/weltraum.jpg" class="w-full h-full object-cover" onerror="this.style.display='none'"><div class="absolute inset-0 flex items-center justify-center bg-indigo-900/50 z-0"><span class="text-5xl">🚀</span></div><span class="absolute bottom-2 left-3 text-xs font-black drop-shadow-md z-10 text-white uppercase tracking-wider">WELTALL</span></div>
<div onclick="window.setBg('../assets/paris.jpg'); playSound('click')" class="aspect-square bg-slate-700 rounded-2xl cursor-pointer border-4 border-transparent hover:border-violet-400 relative group overflow-hidden active:scale-95 transition shadow-lg"><img src="../assets/paris.jpg" class="w-full h-full object-cover" onerror="this.style.display='none'"><div class="absolute inset-0 flex items-center justify-center bg-blue-900/50 z-0"><span class="text-5xl">🇫🇷</span></div><span class="absolute bottom-2 left-3 text-xs font-black drop-shadow-md z-10 text-white uppercase tracking-wider">PARIS</span></div>
<div onclick="window.setBg('../assets/dschungel.jpg'); playSound('click')" class="aspect-square bg-slate-700 rounded-2xl cursor-pointer border-4 border-transparent hover:border-violet-400 relative group overflow-hidden active:scale-95 transition shadow-lg"><img src="../assets/dschungel.jpg" class="w-full h-full object-cover" onerror="this.style.display='none'"><div class="absolute inset-0 flex items-center justify-center bg-green-900/50 z-0"><span class="text-5xl">🌴</span></div><span class="absolute bottom-2 left-3 text-xs font-black drop-shadow-md z-10 text-white uppercase tracking-wider">DSCHUNGEL</span></div>
<div onclick="window.setBg('../assets/unterwasser.jpg'); playSound('click')" class="aspect-square bg-slate-700 rounded-2xl cursor-pointer border-4 border-transparent hover:border-violet-400 relative group overflow-hidden active:scale-95 transition shadow-lg"><img src="../assets/unterwasser.jpg" class="w-full h-full object-cover" onerror="this.style.display='none'"><div class="absolute inset-0 flex items-center justify-center bg-cyan-900/50 z-0"><span class="text-5xl">🐠</span></div><span class="absolute bottom-2 left-3 text-xs font-black drop-shadow-md z-10 text-white uppercase tracking-wider">OZEAN</span></div>
<div onclick="window.setBg('../assets/wolken.jpg'); playSound('click')" class="aspect-square bg-slate-700 rounded-2xl cursor-pointer border-4 border-transparent hover:border-violet-400 relative group overflow-hidden active:scale-95 transition shadow-lg"><img src="../assets/wolken.jpg" class="w-full h-full object-cover" onerror="this.style.display='none'"><div class="absolute inset-0 flex items-center justify-center bg-sky-500/50 z-0"><span class="text-5xl">☁️</span></div><span class="absolute bottom-2 left-3 text-xs font-black drop-shadow-md z-10 text-white uppercase tracking-wider">HIMMEL</span></div>
<div onclick="window.setBg('../assets/schloss.jpg'); playSound('click')" class="aspect-square bg-slate-700 rounded-2xl cursor-pointer border-4 border-transparent hover:border-violet-400 relative group overflow-hidden active:scale-95 transition shadow-lg"><img src="../assets/schloss.jpg" class="w-full h-full object-cover" onerror="this.style.display='none'"><div class="absolute inset-0 flex items-center justify-center bg-purple-900/50 z-0"><span class="text-5xl">🏰</span></div><span class="absolute bottom-2 left-3 text-xs font-black drop-shadow-md z-10 text-white uppercase tracking-wider">SCHLOSS</span></div>
<div onclick="window.setBg('../assets/dino.jpg'); playSound('click')" class="aspect-square bg-slate-700 rounded-2xl cursor-pointer border-4 border-transparent hover:border-violet-400 relative group overflow-hidden active:scale-95 transition shadow-lg"><img src="../assets/dino.jpg" class="w-full h-full object-cover" onerror="this.style.display='none'"><div class="absolute inset-0 flex items-center justify-center bg-amber-900/50 z-0"><span class="text-5xl">🦖</span></div><span class="absolute bottom-2 left-3 text-xs font-black drop-shadow-md z-10 text-white uppercase tracking-wider">DINO</span></div>
<div onclick="window.setBg('../assets/stadion.jpg'); playSound('click')" class="aspect-square bg-slate-700 rounded-2xl cursor-pointer border-4 border-transparent hover:border-violet-400 relative group overflow-hidden active:scale-95 transition shadow-lg"><img src="../assets/stadion.jpg" class="w-full h-full object-cover" onerror="this.style.display='none'"><div class="absolute inset-0 flex items-center justify-center bg-green-800/50 z-0"><span class="text-5xl"></span></div><span class="absolute bottom-2 left-3 text-xs font-black drop-shadow-md z-10 text-white uppercase tracking-wider">STADION</span></div>
</div>
</div>
<div id="info-modal" class="hidden fixed inset-0 z-[200] bg-black/80 backdrop-blur-sm flex items-center justify-center p-4" onclick="window.toggleInfo()">
<div class="bg-slate-800 border-4 border-violet-500 rounded-[2rem] max-w-2xl w-full p-8 shadow-2xl relative" onclick="event.stopPropagation()">
<button onclick="window.toggleInfo(); playSound('click')" class="absolute top-6 right-6 text-white hover:text-violet-400 text-4xl font-bold transition"></button>
<h2 class="text-4xl font-black text-white mb-8 border-b border-slate-600 pb-4 flex items-center gap-4"><span class="text-6xl"></span> ANLEITUNG</h2>
<div class="text-lg text-slate-300 space-y-4">
<p>1. Wähle rechts einen <b>Hintergrund</b>.</p>
<p>2. Die KI schneidet dich automatisch aus.</p>
<p>3. Drücke <b>FOTO</b> für eine Vorschau.</p>
<p>4. Dann kannst du <b>Drucken</b> oder nochmal probieren.</p>
</div>
<button onclick="window.toggleInfo(); playSound('click')" class="mt-8 w-full bg-violet-600 hover:bg-violet-500 text-white font-black py-4 rounded-xl text-xl transition">ALLES KLAR!</button>
</div>
</div>
<div id="print-container" class="hidden"><img id="print-img" src=""></div>
<script>
// --- AUDIO SYSTEM ---
const _soundCache = {
'click': new Audio('../assets/sounds/click.mp3'),
'shutter': new Audio('../assets/sounds/shutter.mp3'),
'success': new Audio('../assets/sounds/success.mp3')
};
Object.values(_soundCache).forEach(s => s.load());
window.playSound = function(id) {
const baseSound = _soundCache[id];
if (baseSound) {
const soundClone = baseSound.cloneNode();
soundClone.volume = 1.0;
soundClone.play().catch(e => console.warn("Sound-Problem:", e));
}
};
// --- HOME FUNKTION ---
window.goHome = function() {
playSound('click');
setTimeout(() => {
window.location.href = '../index.html';
}, 300);
};
let idleTimer;
function resetIdleTimer() {
clearTimeout(idleTimer);
idleTimer = setTimeout(() => {
if (document.getElementById('preview-overlay').classList.contains('hidden')) { window.location.href = '../index.html'; }
}, 90000);
}
['mousedown', 'touchstart', 'scroll', 'keydown', 'input'].forEach(evt => document.addEventListener(evt, window.resetIdleTimer, {passive: true}));
window.resetIdleTimer();
window.toggleInfo = function() {
const modal = document.getElementById('info-modal');
if (modal.classList.contains('hidden')) { modal.classList.remove('hidden'); modal.classList.add('flex'); } else { modal.classList.add('hidden'); modal.classList.remove('flex'); }
}
window.logAi = function(msg, color="white") {
const box = document.getElementById('ai-debug-box');
if(box) { box.innerHTML += `<div><span style="color:${color}">${msg}</span></div>`; box.scrollTop = box.scrollHeight; }
console.log(msg);
}
let seg = null; const video = document.getElementById('ai-video'); const canvas = document.getElementById('ai-canvas'); const ctx = canvas.getContext('2d'); const loadingEl = document.getElementById('ai-loading');
let isRunning = false; let bgImage = new Image(); bgImage.src = "../assets/weltraum.jpg"; let bgColor = null; let loadTimer = null; let currentFacingMode = 'user'; let currentStream;
async function checkFiles() {
const files = ['selfie_segmentation.binarypb', 'selfie_segmentation_solution_simd_wasm_bin.wasm'];
for(let f of files) { try { const r = await fetch('../models/' + f, {method: 'GET', cache: 'no-store'}); if(!r.ok) window.logAi(`WARNUNG: ${f} fehlt evtl.`, "orange"); else window.logAi(`OK: ${f}`, "green"); } catch(e) { window.logAi(`Check ignorieren: ${e.message}`, "gray"); } }
}
async function initCam() {
checkFiles();
try {
if(currentStream) currentStream.getTracks().forEach(t => t.stop());
window.logAi("Starte Kamera...");
const stream = await navigator.mediaDevices.getUserMedia({ video: { width: {ideal: 1280}, height: {ideal: 720}, facingMode: currentFacingMode } });
currentStream = stream; video.srcObject = stream; await video.play();
if(currentFacingMode === 'user') { video.style.transform = 'scaleX(-1)'; } else { video.style.transform = 'scaleX(1)'; }
loadTimer = setTimeout(() => { window.logAi("Timeout! Klicke 'Ohne KI starten'.", "yellow"); }, 30000);
window.logAi("Warte auf Videodaten...");
waitForVideo();
} catch(e) { window.logAi("Kamera-Fehler: " + e.message, "red"); alert("Kamera konnte nicht gestartet werden."); }
}
function waitForVideo() {
if (video.readyState >= 2 && video.videoWidth > 0) { window.logAi("Video läuft. Starte KI..."); setupAI(); } else { requestAnimationFrame(waitForVideo); }
}
window.switchCamera = function() { currentFacingMode = (currentFacingMode === 'user') ? 'environment' : 'user'; initCam(); }
function setupAI() {
if(seg) { loadingEl.style.display = 'none'; isRunning = true; processLoop(); return; }
window.logAi("Lade KI-Modell...", "cyan");
try {
seg = new SelfieSegmentation({locateFile: (file) => `../models/${file}`});
seg.setOptions({ modelSelection: 1, selfieMode: true });
seg.onResults(onResults);
isRunning = true; processLoop();
} catch(e) { window.logAi("KI Init Fehler: " + e, "red"); }
}
async function processLoop() { if(!isRunning) return; if(video.videoWidth > 0 && !video.paused) { try { await seg.send({image: video}); } catch(e) {} } requestAnimationFrame(processLoop); }
function onResults(results) {
if(loadingEl.style.display !== 'none') { clearTimeout(loadTimer); loadingEl.style.display = 'none'; window.logAi("KI läuft!", "green"); }
canvas.width = video.videoWidth; canvas.height = video.videoHeight;
ctx.save(); ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(results.segmentationMask, 0, 0, canvas.width, canvas.height);
ctx.globalCompositeOperation = 'source-in'; ctx.drawImage(results.image, 0, 0, canvas.width, canvas.height);
ctx.globalCompositeOperation = 'destination-over';
if (bgColor) { ctx.fillStyle = bgColor; ctx.fillRect(0, 0, canvas.width, canvas.height); }
else if (bgImage.complete && bgImage.naturalWidth > 0) { ctx.drawImage(bgImage, 0, 0, canvas.width, canvas.height); }
else { ctx.fillStyle = '#111'; ctx.fillRect(0, 0, canvas.width, canvas.height); }
ctx.restore();
}
window.setBg = function(path, color = null) { if (color) { bgColor = color; } else { bgColor = null; bgImage.src = path; } }
window.capturePhoto = function() {
playSound('shutter');
flashEffect(); isRunning = false; const dataUrl = canvas.toDataURL('image/png');
document.getElementById('preview-img-display').src = dataUrl;
document.getElementById('preview-overlay').classList.remove('hidden'); document.getElementById('preview-overlay').style.display = 'flex';
document.getElementById('main-controls').classList.add('hidden');
}
window.closePreview = function() {
document.getElementById('preview-overlay').classList.add('hidden'); document.getElementById('preview-overlay').style.display = 'none';
document.getElementById('main-controls').classList.remove('hidden'); isRunning = true; processLoop();
}
window.saveFromPreview = function() {
playSound('success');
const dataUrl = document.getElementById('preview-img-display').src; const link = document.createElement('a'); link.download = 'magic-selfie.png'; link.href = dataUrl; link.click();
}
window.printFromPreview = async function(el) {
playSound('success');
const btn = el || document.getElementById('btn-print'); const oldText = btn ? btn.innerText : "🖨️ DRUCKEN";
if (btn) { btn.innerText = "⏳ Sende..."; btn.disabled = true; }
const dataUrl = document.getElementById('preview-img-display').src;
try {
if (window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.DirectPrinter) {
await window.Capacitor.Plugins.DirectPrinter.printBase64({ data: dataUrl, name: 'Magic-Selfie' });
if (btn) btn.innerText = "✅ Gedruckt!";
} else {
console.warn("DirectPrinter Plugin nicht gefunden, nutze Browser Print.");
document.getElementById('print-img').src = dataUrl; setTimeout(() => window.print(), 250);
if (btn) btn.innerText = "✅ OK";
}
} catch (error) { console.error("Druckfehler:", error); alert("Drucken fehlgeschlagen: " + error.message); if (btn) btn.innerText = "❌ Fehler"; }
setTimeout(() => { if (btn) { btn.innerText = oldText; btn.disabled = false; } }, 3000);
}
window.takePhoto = function() { window.capturePhoto(); }
function flashEffect() { canvas.style.filter = "brightness(10)"; setTimeout(() => canvas.style.filter = "none", 100); }
window.drawFallback = function() {
isRunning = false; loadingEl.style.display = 'none'; window.logAi("KI deaktiviert.");
function loop() {
canvas.width = video.videoWidth; canvas.height = video.videoHeight;
ctx.save(); if(currentFacingMode === 'user') ctx.scale(-1, 1);
ctx.drawImage(video, currentFacingMode === 'user' ? -canvas.width : 0, 0); ctx.restore();
ctx.fillStyle = "white"; ctx.font = "20px sans-serif"; ctx.fillText("Ohne KI", 20, 40);
requestAnimationFrame(loop);
} loop();
}
initCam();
</script>
</body>
</html>

258
public/apps/magic_old.html Normal file
View File

@@ -0,0 +1,258 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Magic Booth</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<script src="../js/tailwind.js"></script>
<script src="../js/camera_utils.js"></script>
<script src="../js/selfie_segmentation.js"></script>
<style>
/* HIER gehört das CSS hin (unsichtbar für den Nutzer) */
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
body {
background-color: #facc15;
overflow: hidden;
user-select: none;
touch-action: none;
transition: background-color 0.5s;
animation: fadeIn 0.4s ease-out; /* Die Animation */
}
body { background-color: #0f172a; color: white; overflow: hidden; user-select: none; touch-action: none; }
.no-scrollbar::-webkit-scrollbar { display: none; }
button:active { transform: scale(0.95); transition: transform 0.1s; }
@media print {
body * { visibility: hidden; }
#print-container, #print-container * { visibility: visible; }
#print-container { position: fixed; left: 0; top: 0; width: 100%; height: 100%; display: flex !important; align-items: center; justify-content: center; background: white; z-index: 99999; }
#print-img { max-width: 100%; max-height: 100%; object-fit: contain; }
}
</style>
</head>
<body class="h-screen w-screen flex flex-col md:flex-row p-0 m-0 bg-slate-900" onclick="window.resetIdleTimer()" ontouchstart="window.resetIdleTimer()">
<button onclick="goHome()" class="absolute top-6 left-6 z-[100] bg-slate-800 text-white border-4 border-slate-600 hover:border-white px-8 py-4 rounded-full text-2xl font-black shadow-2xl flex items-center gap-3 transition-transform active:scale-95 no-underline">
🏠 MENÜ
</button>
<button onclick="window.toggleInfo(); playSound('click')" class="absolute top-6 right-6 z-[100] w-14 h-14 bg-slate-800 text-white rounded-full font-bold border-4 border-slate-600 hover:border-white shadow-xl flex items-center justify-center transition-transform active:scale-95 text-2xl">
?
</button>
<div class="relative flex-1 bg-black flex flex-col items-center justify-center overflow-hidden h-[60%] md:h-full w-full">
<div class="relative w-full h-full flex items-center justify-center p-2 md:p-8 pb-24">
<video id="ai-video" autoplay playsinline muted class="hidden"></video>
<canvas id="ai-canvas" class="max-w-full max-h-full object-contain shadow-2xl rounded-lg"></canvas>
<div id="preview-overlay" class="absolute inset-0 bg-slate-900 z-50 hidden flex-col items-center justify-center">
<h2 class="text-3xl font-bold text-white mb-6 animate-bounce">Dein magisches Foto!</h2>
<div class="relative max-w-[90%] max-h-[60%] border-8 border-white shadow-2xl rounded-xl overflow-hidden bg-black">
<img id="preview-img-display" class="w-full h-full object-contain" src="">
</div>
<div class="flex gap-6 mt-8">
<button onclick="window.closePreview(); playSound('click')" class="bg-yellow-500 hover:bg-yellow-400 text-black font-black py-4 px-8 rounded-2xl text-xl shadow-xl active:scale-95 transition border-b-8 border-yellow-700 active:border-b-0 active:translate-y-2 flex items-center gap-2">🔄 NOCHMAL</button>
<button id="btn-print" onclick="window.printFromPreview(this)" class="bg-white text-slate-900 font-black py-4 px-8 rounded-2xl text-xl shadow-xl hover:bg-gray-100 active:scale-95 transition border-b-8 border-gray-400 active:border-b-0 active:translate-y-2 flex items-center gap-2">🖨️ DRUCKEN</button>
<button onclick="window.saveFromPreview()" class="bg-violet-600 text-white font-black py-4 px-8 rounded-2xl text-xl shadow-xl hover:bg-violet-500 active:scale-95 transition border-b-8 border-violet-800 active:border-b-0 active:translate-y-2 flex items-center gap-2">💾 FOTO</button>
</div>
</div>
</div>
<div id="ai-loading" class="absolute inset-0 bg-black/95 flex flex-col items-center justify-center z-40 p-4 text-center">
<div class="text-6xl animate-spin mb-4">🔮</div>
<div class="text-2xl font-bold mb-4">Lade KI...</div>
<div id="ai-debug-box" class="text-xs text-left font-mono bg-black/80 border border-gray-700 p-4 rounded-xl mt-4 max-w-lg w-full h-32 overflow-y-auto opacity-70"><div>[Log] App gestartet...</div></div>
<div class="mt-8 flex gap-6">
<button onclick="location.reload()" class="border-2 border-white/30 px-6 py-3 rounded-full hover:bg-white/10">Neu laden</button>
<button onclick="window.drawFallback(); document.getElementById('ai-loading').style.display='none'; playSound('click')" class="bg-red-600 px-6 py-3 rounded-full font-bold hover:bg-red-500">Ohne KI starten</button>
</div>
</div>
<div id="main-controls" class="absolute bottom-6 flex gap-4 z-30 w-full justify-center px-4">
<button onclick="window.switchCamera(); playSound('click')" class="w-20 bg-slate-700 text-white font-black py-4 rounded-2xl text-2xl shadow-xl hover:scale-105 active:scale-95 transition border-b-8 border-slate-900 active:border-b-0 active:translate-y-2 flex items-center justify-center" title="Kamera wechseln">🔄</button>
<button onclick="window.capturePhoto()" class="flex-1 max-w-[200px] bg-violet-600 text-white font-black py-4 rounded-2xl text-lg md:text-xl shadow-xl hover:scale-105 active:scale-95 transition border-b-8 border-violet-800 active:border-b-0 active:translate-y-2 flex items-center justify-center gap-2">✨ FOTO</button>
</div>
</div>
<div class="h-[40%] md:h-full md:w-1/3 min-w-[280px] max-w-md bg-slate-800 p-4 border-t-4 md:border-t-0 md:border-l-4 border-slate-700 overflow-y-auto no-scrollbar z-20 flex flex-col">
<h3 class="text-white font-black text-xl mb-4 text-center uppercase tracking-wider sticky top-0 bg-slate-800 pb-2 z-10 pt-2">🌍 Reiseziel</h3>
<div class="grid grid-cols-2 gap-3 pb-4">
<div onclick="window.setBg('../assets/weltraum.jpg'); playSound('click')" class="aspect-square bg-slate-700 rounded-2xl cursor-pointer border-4 border-transparent hover:border-violet-400 relative group overflow-hidden active:scale-95 transition shadow-lg"><img src="../assets/weltraum.jpg" class="w-full h-full object-cover" onerror="this.style.display='none'"><div class="absolute inset-0 flex items-center justify-center bg-indigo-900/50 z-0"><span class="text-5xl">🚀</span></div><span class="absolute bottom-2 left-3 text-xs font-black drop-shadow-md z-10 text-white uppercase tracking-wider">WELTALL</span></div>
<div onclick="window.setBg('../assets/paris.jpg'); playSound('click')" class="aspect-square bg-slate-700 rounded-2xl cursor-pointer border-4 border-transparent hover:border-violet-400 relative group overflow-hidden active:scale-95 transition shadow-lg"><img src="../assets/paris.jpg" class="w-full h-full object-cover" onerror="this.style.display='none'"><div class="absolute inset-0 flex items-center justify-center bg-blue-900/50 z-0"><span class="text-5xl">🇫🇷</span></div><span class="absolute bottom-2 left-3 text-xs font-black drop-shadow-md z-10 text-white uppercase tracking-wider">PARIS</span></div>
<div onclick="window.setBg('../assets/dschungel.jpg'); playSound('click')" class="aspect-square bg-slate-700 rounded-2xl cursor-pointer border-4 border-transparent hover:border-violet-400 relative group overflow-hidden active:scale-95 transition shadow-lg"><img src="../assets/dschungel.jpg" class="w-full h-full object-cover" onerror="this.style.display='none'"><div class="absolute inset-0 flex items-center justify-center bg-green-900/50 z-0"><span class="text-5xl">🌴</span></div><span class="absolute bottom-2 left-3 text-xs font-black drop-shadow-md z-10 text-white uppercase tracking-wider">DSCHUNGEL</span></div>
<div onclick="window.setBg('../assets/unterwasser.jpg'); playSound('click')" class="aspect-square bg-slate-700 rounded-2xl cursor-pointer border-4 border-transparent hover:border-violet-400 relative group overflow-hidden active:scale-95 transition shadow-lg"><img src="../assets/unterwasser.jpg" class="w-full h-full object-cover" onerror="this.style.display='none'"><div class="absolute inset-0 flex items-center justify-center bg-cyan-900/50 z-0"><span class="text-5xl">🐠</span></div><span class="absolute bottom-2 left-3 text-xs font-black drop-shadow-md z-10 text-white uppercase tracking-wider">OZEAN</span></div>
<div onclick="window.setBg('../assets/wolken.jpg'); playSound('click')" class="aspect-square bg-slate-700 rounded-2xl cursor-pointer border-4 border-transparent hover:border-violet-400 relative group overflow-hidden active:scale-95 transition shadow-lg"><img src="../assets/wolken.jpg" class="w-full h-full object-cover" onerror="this.style.display='none'"><div class="absolute inset-0 flex items-center justify-center bg-sky-500/50 z-0"><span class="text-5xl">☁️</span></div><span class="absolute bottom-2 left-3 text-xs font-black drop-shadow-md z-10 text-white uppercase tracking-wider">HIMMEL</span></div>
<div onclick="window.setBg('../assets/schloss.jpg'); playSound('click')" class="aspect-square bg-slate-700 rounded-2xl cursor-pointer border-4 border-transparent hover:border-violet-400 relative group overflow-hidden active:scale-95 transition shadow-lg"><img src="../assets/schloss.jpg" class="w-full h-full object-cover" onerror="this.style.display='none'"><div class="absolute inset-0 flex items-center justify-center bg-purple-900/50 z-0"><span class="text-5xl">🏰</span></div><span class="absolute bottom-2 left-3 text-xs font-black drop-shadow-md z-10 text-white uppercase tracking-wider">SCHLOSS</span></div>
<div onclick="window.setBg('../assets/dino.jpg'); playSound('click')" class="aspect-square bg-slate-700 rounded-2xl cursor-pointer border-4 border-transparent hover:border-violet-400 relative group overflow-hidden active:scale-95 transition shadow-lg"><img src="../assets/dino.jpg" class="w-full h-full object-cover" onerror="this.style.display='none'"><div class="absolute inset-0 flex items-center justify-center bg-amber-900/50 z-0"><span class="text-5xl">🦖</span></div><span class="absolute bottom-2 left-3 text-xs font-black drop-shadow-md z-10 text-white uppercase tracking-wider">DINO</span></div>
<div onclick="window.setBg('../assets/stadion.jpg'); playSound('click')" class="aspect-square bg-slate-700 rounded-2xl cursor-pointer border-4 border-transparent hover:border-violet-400 relative group overflow-hidden active:scale-95 transition shadow-lg"><img src="../assets/stadion.jpg" class="w-full h-full object-cover" onerror="this.style.display='none'"><div class="absolute inset-0 flex items-center justify-center bg-green-800/50 z-0"><span class="text-5xl"></span></div><span class="absolute bottom-2 left-3 text-xs font-black drop-shadow-md z-10 text-white uppercase tracking-wider">STADION</span></div>
</div>
</div>
<div id="info-modal" class="hidden fixed inset-0 z-[200] bg-black/80 backdrop-blur-sm flex items-center justify-center p-4" onclick="window.toggleInfo()">
<div class="bg-slate-800 border-4 border-violet-500 rounded-[2rem] max-w-2xl w-full p-8 shadow-2xl relative" onclick="event.stopPropagation()">
<button onclick="window.toggleInfo(); playSound('click')" class="absolute top-6 right-6 text-white hover:text-violet-400 text-4xl font-bold transition"></button>
<h2 class="text-4xl font-black text-white mb-8 border-b border-slate-600 pb-4 flex items-center gap-4"><span class="text-6xl"></span> ANLEITUNG</h2>
<div class="text-lg text-slate-300 space-y-4">
<p>1. Wähle rechts einen <b>Hintergrund</b>.</p>
<p>2. Die KI schneidet dich automatisch aus.</p>
<p>3. Drücke <b>FOTO</b> für eine Vorschau.</p>
<p>4. Dann kannst du <b>Drucken</b> oder nochmal probieren.</p>
</div>
<button onclick="window.toggleInfo(); playSound('click')" class="mt-8 w-full bg-violet-600 hover:bg-violet-500 text-white font-black py-4 rounded-xl text-xl transition">ALLES KLAR!</button>
</div>
</div>
<div id="print-container" class="hidden"><img id="print-img" src=""></div>
<script>
// --- ROBUSTES AUDIO SYSTEM (ANTI-ABSTURZ) ---
const _soundCache = {
'click': new Audio('../assets/sounds/click.mp3'),
'shutter': new Audio('../assets/sounds/shutter.mp3'),
'success': new Audio('../assets/sounds/success.mp3')
};
// Sounds vorladen
Object.values(_soundCache).forEach(s => s.load());
// Abspiel-Funktion
window.playSound = function(id) {
const baseSound = _soundCache[id];
if (baseSound) {
const soundClone = baseSound.cloneNode();
soundClone.volume = 1.0;
soundClone.play().catch(e => console.warn("Sound-Problem:", e));
}
};
// --- HOME FUNKTION (Diese fehlte wahrscheinlich!) ---
window.goHome = function() {
playSound('click');
// Kleine Verzögerung, damit der Sound noch zu hören ist
setTimeout(() => {
window.location.href = '../index.html';
}, 300);
};
let idleTimer;
function resetIdleTimer() {
clearTimeout(idleTimer);
idleTimer = setTimeout(() => {
if (document.getElementById('preview-overlay').classList.contains('hidden')) { window.location.href = '../index.html'; }
}, 90000);
}
['mousedown', 'touchstart', 'scroll', 'keydown', 'input'].forEach(evt => document.addEventListener(evt, window.resetIdleTimer, {passive: true}));
window.resetIdleTimer();
window.toggleInfo = function() {
const modal = document.getElementById('info-modal');
if (modal.classList.contains('hidden')) { modal.classList.remove('hidden'); modal.classList.add('flex'); } else { modal.classList.add('hidden'); modal.classList.remove('flex'); }
}
window.logAi = function(msg, color="white") {
const box = document.getElementById('ai-debug-box');
if(box) { box.innerHTML += `<div><span style="color:${color}">${msg}</span></div>`; box.scrollTop = box.scrollHeight; }
console.log(msg);
}
let seg = null; const video = document.getElementById('ai-video'); const canvas = document.getElementById('ai-canvas'); const ctx = canvas.getContext('2d'); const loadingEl = document.getElementById('ai-loading');
let isRunning = false; let bgImage = new Image(); bgImage.src = "../assets/weltraum.jpg"; let bgColor = null; let loadTimer = null; let currentFacingMode = 'user'; let currentStream;
async function checkFiles() {
const files = ['selfie_segmentation.binarypb', 'selfie_segmentation_solution_simd_wasm_bin.wasm'];
for(let f of files) { try { const r = await fetch('../models/' + f, {method: 'GET', cache: 'no-store'}); if(!r.ok) window.logAi(`WARNUNG: ${f} fehlt evtl.`, "orange"); else window.logAi(`OK: ${f}`, "green"); } catch(e) { window.logAi(`Check ignorieren: ${e.message}`, "gray"); } }
}
async function initCam() {
checkFiles();
try {
if(currentStream) currentStream.getTracks().forEach(t => t.stop());
window.logAi("Starte Kamera...");
const stream = await navigator.mediaDevices.getUserMedia({ video: { width: {ideal: 1280}, height: {ideal: 720}, facingMode: currentFacingMode } });
currentStream = stream; video.srcObject = stream; await video.play();
if(currentFacingMode === 'user') { video.style.transform = 'scaleX(-1)'; } else { video.style.transform = 'scaleX(1)'; }
loadTimer = setTimeout(() => { window.logAi("Timeout! Klicke 'Ohne KI starten'.", "yellow"); }, 30000);
window.logAi("Warte auf Videodaten...");
waitForVideo();
} catch(e) { window.logAi("Kamera-Fehler: " + e.message, "red"); alert("Kamera konnte nicht gestartet werden."); }
}
function waitForVideo() {
if (video.readyState >= 2 && video.videoWidth > 0) { window.logAi("Video läuft. Starte KI..."); setupAI(); } else { requestAnimationFrame(waitForVideo); }
}
window.switchCamera = function() { currentFacingMode = (currentFacingMode === 'user') ? 'environment' : 'user'; initCam(); }
function setupAI() {
if(seg) { loadingEl.style.display = 'none'; isRunning = true; processLoop(); return; }
window.logAi("Lade KI-Modell...", "cyan");
try {
seg = new SelfieSegmentation({locateFile: (file) => `../models/${file}`});
seg.setOptions({ modelSelection: 1, selfieMode: true });
seg.onResults(onResults);
isRunning = true; processLoop();
} catch(e) { window.logAi("KI Init Fehler: " + e, "red"); }
}
async function processLoop() { if(!isRunning) return; if(video.videoWidth > 0 && !video.paused) { try { await seg.send({image: video}); } catch(e) {} } requestAnimationFrame(processLoop); }
function onResults(results) {
if(loadingEl.style.display !== 'none') { clearTimeout(loadTimer); loadingEl.style.display = 'none'; window.logAi("KI läuft!", "green"); }
canvas.width = video.videoWidth; canvas.height = video.videoHeight;
ctx.save(); ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(results.segmentationMask, 0, 0, canvas.width, canvas.height);
ctx.globalCompositeOperation = 'source-in'; ctx.drawImage(results.image, 0, 0, canvas.width, canvas.height);
ctx.globalCompositeOperation = 'destination-over';
if (bgColor) { ctx.fillStyle = bgColor; ctx.fillRect(0, 0, canvas.width, canvas.height); }
else if (bgImage.complete && bgImage.naturalWidth > 0) { ctx.drawImage(bgImage, 0, 0, canvas.width, canvas.height); }
else { ctx.fillStyle = '#111'; ctx.fillRect(0, 0, canvas.width, canvas.height); }
ctx.restore();
}
window.setBg = function(path, color = null) { if (color) { bgColor = color; } else { bgColor = null; bgImage.src = path; } }
window.capturePhoto = function() {
playSound('shutter'); // SOUND
flashEffect(); isRunning = false; const dataUrl = canvas.toDataURL('image/png');
document.getElementById('preview-img-display').src = dataUrl;
document.getElementById('preview-overlay').classList.remove('hidden'); document.getElementById('preview-overlay').style.display = 'flex';
document.getElementById('main-controls').classList.add('hidden');
}
window.closePreview = function() {
document.getElementById('preview-overlay').classList.add('hidden'); document.getElementById('preview-overlay').style.display = 'none';
document.getElementById('main-controls').classList.remove('hidden'); isRunning = true; processLoop();
}
window.saveFromPreview = function() {
playSound('success'); // SOUND
const dataUrl = document.getElementById('preview-img-display').src; const link = document.createElement('a'); link.download = 'magic-booth.png'; link.href = dataUrl; link.click();
}
window.printFromPreview = async function(el) {
playSound('success'); // SOUND
const btn = el || document.getElementById('btn-print'); const oldText = btn ? btn.innerText : "🖨️ DRUCKEN";
if (btn) { btn.innerText = "⏳ Sende..."; btn.disabled = true; }
const dataUrl = document.getElementById('preview-img-display').src;
try {
if (window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.DirectPrinter) {
await window.Capacitor.Plugins.DirectPrinter.printBase64({ data: dataUrl, name: 'Magic-Booth' });
if (btn) btn.innerText = "✅ Gedruckt!";
} else {
console.warn("DirectPrinter Plugin nicht gefunden, nutze Browser Print.");
document.getElementById('print-img').src = dataUrl; setTimeout(() => window.print(), 250);
if (btn) btn.innerText = "✅ OK";
}
} catch (error) { console.error("Druckfehler:", error); alert("Drucken fehlgeschlagen: " + error.message); if (btn) btn.innerText = "❌ Fehler"; }
setTimeout(() => { if (btn) { btn.innerText = oldText; btn.disabled = false; } }, 3000);
}
window.takePhoto = function() { window.capturePhoto(); }
function flashEffect() { canvas.style.filter = "brightness(10)"; setTimeout(() => canvas.style.filter = "none", 100); }
window.drawFallback = function() {
isRunning = false; loadingEl.style.display = 'none'; window.logAi("KI deaktiviert.");
function loop() {
canvas.width = video.videoWidth; canvas.height = video.videoHeight;
ctx.save(); if(currentFacingMode === 'user') ctx.scale(-1, 1);
ctx.drawImage(video, currentFacingMode === 'user' ? -canvas.width : 0, 0); ctx.restore();
ctx.fillStyle = "white"; ctx.font = "20px sans-serif"; ctx.fillText("Ohne KI", 20, 40);
requestAnimationFrame(loop);
} loop();
}
initCam();
</script>
</body>
</html>

154
public/apps/news.html Normal file
View File

@@ -0,0 +1,154 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>News Studio</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<script src="../js/tailwind.js"></script>
<style>
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
html { width: 100%; height: 100%; background-color: #0f172a; }
body {
position: fixed; top: 0; left: 0; right: 0; bottom: 0; margin: 0;
background-color: #0f172a;
color: white;
overflow: hidden !important;
user-select: none;
touch-action: manipulation;
font-family: sans-serif;
animation: fadeIn 0.4s ease-out;
box-sizing: border-box;
padding-top: calc(10px + env(safe-area-inset-top));
padding-bottom: max(20px, env(safe-area-inset-bottom));
}
.no-scrollbar::-webkit-scrollbar { display: none; }
button:active { transform: scale(0.95); transition: transform 0.1s; }
.animate-blink { animation: blink 1.5s infinite; }
@keyframes blink { 0% { opacity: 1; } 50% { opacity: 0; } 100% { opacity: 1; } }
.ticker-wrap { width: 100%; overflow: hidden; background-color: #fbbf24; color: black; font-weight: 900; white-space: nowrap; box-sizing: border-box; }
.ticker { display: inline-block; padding-left: 100%; animation: ticker 25s linear infinite; }
@keyframes ticker { 0% { transform: translate3d(0, 0, 0); } 100% { transform: translate3d(-100%, 0, 0); } }
.editable-input { background: transparent; border: none; color: inherit; width: 100%; text-align: center; outline: none; font-family: inherit; text-transform: uppercase; text-overflow: ellipsis; }
.editable-input:focus { background: rgba(255,255,255,0.1); border-radius: 4px; }
</style>
</head>
<body class="flex flex-col"
onclick="if(window.resetIdleTimer) window.resetIdleTimer()"
ontouchstart="if(window.resetIdleTimer) window.resetIdleTimer()">
<div class="flex-none flex justify-between items-center z-50 px-4 relative bg-slate-900 shadow-md h-16">
<button onclick="goHome()" class="bg-slate-800 text-white border-4 border-slate-600 hover:border-white px-4 py-2 rounded-full text-lg font-black shadow-xl flex items-center gap-2 transition-transform active:scale-95 no-underline focus:outline-none shrink-0 z-50">
🏠 MENÜ
</button>
<div class="absolute inset-0 flex items-center justify-center pointer-events-none z-0">
<h1 class="text-2xl md:text-4xl font-black text-blue-400 font-sans tracking-tighter drop-shadow-[0_4px_4px_rgba(0,0,0,0.8)] uppercase italic transform -skew-x-6">
📰 NEWS
</h1>
</div>
<button onclick="toggleInfo(); playSound('click')" class="w-12 h-12 bg-slate-800 text-white rounded-full font-bold border-4 border-slate-600 hover:border-white shadow-xl flex items-center justify-center transition-transform active:scale-95 text-xl shrink-0 z-50">
?
</button>
</div>
<div class="flex-1 relative w-full overflow-hidden bg-black shadow-inner">
<div class="absolute inset-0 z-0">
<video id="video" autoplay playsinline muted class="w-full h-full object-cover opacity-90"></video>
<canvas id="canvas" class="hidden"></canvas>
</div>
<div class="absolute inset-0 z-20 flex flex-col justify-between pointer-events-none pb-4">
<div class="w-full flex justify-end p-4 md:p-6">
<div class="flex items-center gap-2 bg-red-600/90 px-4 py-2 rounded-lg shadow-lg border-2 border-red-400 pointer-events-auto transform rotate-1 hover:rotate-0 transition-transform">
<div class="w-3 h-3 md:w-4 md:h-4 bg-white rounded-full animate-blink"></div>
<span class="font-black text-lg md:text-xl tracking-widest text-white">LIVE</span>
</div>
</div>
<div class="flex flex-col w-full pointer-events-auto items-center">
<div class="w-[90%] md:max-w-5xl bg-blue-800/95 border-l-8 border-blue-500 p-2 md:p-4 mb-2 shadow-2xl transform -skew-x-6 relative group hover:bg-blue-700/95 transition-colors">
<div class="text-blue-300 text-xs md:text-sm font-bold uppercase tracking-[0.2em] mb-1 ml-2 not-italic transform skew-x-6">Breaking News</div>
<input type="text" id="headline-input" value="SENSATION IM JUGENDZENTRUM!" class="editable-input text-2xl md:text-4xl font-black text-white drop-shadow-md transform skew-x-6 placeholder-blue-300">
</div>
<div class="self-start ml-4 md:ml-16 w-auto max-w-[80%] bg-red-600/95 text-white font-bold px-6 py-2 transform -skew-x-6 shadow-xl text-lg md:text-xl border-l-4 border-red-300 mb-4 hover:bg-red-500/95 transition-colors">
<input type="text" value="🔴 VOR ORT: MEDIENSTATION" class="editable-input w-full text-left transform skew-x-6">
</div>
<div class="ticker-wrap bg-yellow-400 py-2 md:py-3 border-y-4 border-black shadow-xl mb-4 md:mb-6">
<div class="ticker text-xl md:text-3xl text-black">
+++ WETTER: ES REGNET KONFETTI +++ SCHULE FÄLLT HEUTE AUS +++ EISCREME FÜR ALLE +++ KATZE WIRD BÜRGERMEISTER +++ EXTRA BLATT +++
</div>
</div>
<div class="flex justify-center gap-4 md:gap-6 pb-2 md:pb-4 w-full">
<button onclick="switchCamera(); playSound('click')" class="bg-slate-800/90 hover:bg-slate-700 text-white w-14 h-14 md:w-16 md:h-16 rounded-full border-2 border-slate-500 shadow-xl backdrop-blur flex items-center justify-center active:scale-95 transition">
<span class="text-2xl md:text-3xl">🔄</span>
</button>
<button onclick="capturePreview()" class="bg-white hover:bg-gray-200 text-black font-black h-14 md:h-16 px-8 md:px-10 rounded-full border-b-8 border-gray-400 active:border-b-0 active:translate-y-2 shadow-2xl text-lg md:text-xl flex items-center gap-3 transition active:scale-95">
<span>📸</span> FOTO
</button>
</div>
</div>
</div>
</div>
<div id="preview-modal" class="hidden fixed inset-0 z-[100] bg-black/90 flex flex-col items-center justify-center p-4 backdrop-blur-md">
<button onclick="goHome()" class="absolute top-6 left-6 z-50 bg-blue-900/90 backdrop-blur border-4 border-blue-500 hover:border-white px-4 py-2 rounded-full text-lg font-black shadow-xl flex items-center gap-2 transition-transform active:scale-95 text-white">
🏠 MENÜ
</button>
<h2 class="text-white text-3xl font-black mb-4">DEIN FOTO</h2>
<div class="relative w-full max-w-4xl h-auto border-8 border-white shadow-2xl rounded-lg overflow-hidden bg-black mb-8">
<img id="preview-img" src="" class="w-full h-auto object-contain">
</div>
<div class="flex gap-6">
<button onclick="closePreview(); playSound('click')" class="bg-red-600 hover:bg-red-500 text-white font-black py-4 px-8 rounded-2xl text-xl shadow-lg border-b-8 border-red-800 active:border-b-0 active:translate-y-2 transition flex items-center gap-2">
🗑️ LÖSCHEN
</button>
<button id="btn-print-confirm" onclick="confirmPrint()" class="bg-green-600 hover:bg-green-500 text-white font-black py-4 px-12 rounded-2xl text-xl shadow-lg border-b-8 border-green-800 active:border-b-0 active:translate-y-2 transition flex items-center gap-2">
🖨️ DRUCKEN
</button>
</div>
</div>
<div id="info-modal" class="hidden fixed inset-0 z-[200] bg-black/80 backdrop-blur-sm items-center justify-center p-4" onclick="toggleInfo()">
<div class="bg-slate-800 border-4 border-blue-500 rounded-[2rem] max-w-lg w-full p-8 shadow-2xl relative" onclick="event.stopPropagation()">
<button onclick="toggleInfo(); playSound('click')" class="absolute top-6 right-6 text-white hover:text-blue-400 text-4xl font-bold transition"></button>
<h2 class="text-3xl font-black text-white mb-6 border-b border-slate-600 pb-4">📰 NEWS STUDIO</h2>
<ul class="text-slate-300 space-y-3 mb-8 list-disc pl-6 text-lg">
<li>Tippe auf die <b>blaue Schlagzeile</b> und schreibe deinen Text.</li>
<li>Tippe auf den <b>roten Ort</b>.</li>
<li>Drücke auf <b>FOTO</b> für eine Vorschau.</li>
<li>Wenn es dir gefällt, drücke auf <b>DRUCKEN</b>.</li>
</ul>
<button onclick="toggleInfo(); playSound('click')" class="w-full bg-blue-600 hover:bg-blue-500 text-white font-black py-4 rounded-xl text-xl transition">ALLES KLAR!</button>
</div>
</div>
<script>
const _soundCache = {'click': new Audio('../assets/sounds/click.mp3'), 'shutter': new Audio('../assets/sounds/shutter.mp3'), 'success': new Audio('../assets/sounds/success.mp3')};
Object.values(_soundCache).forEach(s => s.load());
window.playSound = function(id) { if(_soundCache[id]) { const s = _soundCache[id].cloneNode(); s.volume = 1.0; s.play().catch(e=>{}); } };
window.goHome = function() { playSound('click'); setTimeout(() => { window.location.href = '../index.html'; }, 300); };
let idleTimer;
window.resetIdleTimer = function() { clearTimeout(idleTimer); idleTimer = setTimeout(() => { window.location.href = '../index.html'; }, 120000); };
['mousedown', 'touchstart', 'scroll', 'keydown', 'input'].forEach(evt => document.addEventListener(evt, window.resetIdleTimer, {passive: true})); window.resetIdleTimer();
function toggleInfo() { const m = document.getElementById('info-modal'); m.classList.toggle('hidden'); m.classList.toggle('flex'); }
let video = document.getElementById('video'); let currentStream; let currentFacingMode = 'user';
async function initCam() { try { if (currentStream && currentStream.getTracks) currentStream.getTracks().forEach(track => track.stop()); const stream = await navigator.mediaDevices.getUserMedia({ video: { width: {ideal: 1280}, height: {ideal: 720}, facingMode: currentFacingMode } }); currentStream = stream; video.srcObject = stream; video.style.transform = (currentFacingMode === 'user') ? 'scaleX(-1)' : 'scaleX(1)'; } catch (err) { console.error("Kamera Fehler:", err); } }
function switchCamera() { currentFacingMode = (currentFacingMode === 'user') ? 'environment' : 'user'; initCam(); }
function capturePreview() { playSound('shutter'); const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); canvas.width = video.videoWidth || 1280; canvas.height = video.videoHeight || 720; ctx.save(); if (currentFacingMode === 'user') { ctx.translate(canvas.width, 0); ctx.scale(-1, 1); } ctx.drawImage(video, 0, 0, canvas.width, canvas.height); ctx.restore(); const headline = document.getElementById('headline-input').value.toUpperCase(); ctx.fillStyle = "rgba(30, 64, 175, 0.95)"; const boxH = 140; const boxY = canvas.height - 220; const boxMargin = 60; const boxW = canvas.width - (boxMargin * 2); ctx.save(); ctx.translate(boxMargin, boxY); ctx.transform(1, 0, -0.1, 1, 0, 0); ctx.fillRect(0, 0, boxW, boxH); ctx.fillStyle = "#3b82f6"; ctx.fillRect(0, 0, 25, boxH); ctx.restore(); ctx.fillStyle = "white"; ctx.font = "900 55px sans-serif"; ctx.textAlign = "center"; ctx.fillText(headline, canvas.width / 2, boxY + 90, boxW - 100); ctx.fillStyle = "#93c5fd"; ctx.font = "bold 24px sans-serif"; ctx.textAlign = "left"; ctx.fillText("BREAKING NEWS", boxMargin + 40, boxY + 35); const redW = 550; const redY = boxY - 65; const redX = boxMargin + 40; ctx.save(); ctx.translate(redX, redY); ctx.transform(1, 0, -0.1, 1, 0, 0); ctx.fillStyle = "rgba(220, 38, 38, 0.95)"; ctx.fillRect(0, 0, redW, 60); ctx.fillStyle = "#fca5a5"; ctx.fillRect(0, 0, 10, 60); ctx.restore(); ctx.fillStyle = "white"; ctx.font = "bold 30px sans-serif"; ctx.textAlign = "left"; ctx.fillText("🔴 VOR ORT: MEDIENSTATION", redX + 30, redY + 40); ctx.fillStyle = "#fbbf24"; ctx.fillRect(0, canvas.height - 70, canvas.width, 70); ctx.fillStyle = "black"; ctx.font = "900 35px sans-serif"; ctx.textAlign = "left"; ctx.fillText("+++ WETTER: ES REGNET KONFETTI +++ SCHULE FÄLLT HEUTE AUS +++ EXTRA BLATT +++", 20, canvas.height - 22); ctx.fillStyle = "#dc2626"; ctx.fillRect(canvas.width - 160, 40, 120, 50); ctx.fillStyle = "white"; ctx.font = "900 30px sans-serif"; ctx.textAlign = "center"; ctx.fillText("LIVE", canvas.width - 100, 75); const dataUrl = canvas.toDataURL('image/png'); document.getElementById('preview-img').src = dataUrl; document.getElementById('preview-modal').classList.remove('hidden'); document.getElementById('preview-modal').classList.add('flex'); }
function closePreview() { document.getElementById('preview-modal').classList.add('hidden'); document.getElementById('preview-modal').classList.remove('flex'); }
async function confirmPrint() { const btn = document.getElementById('btn-print-confirm'); btn.innerText = "⏳ DRUCKE..."; const img = document.getElementById('preview-img'); const dataUrl = img.src; try { if (window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.DirectPrinter) { await window.Capacitor.Plugins.DirectPrinter.printBase64({ data: dataUrl, name: 'News-Flash' }); } else { const link = document.createElement('a'); link.download = 'breaking-news.png'; link.href = dataUrl; link.click(); } playSound('success'); setTimeout(() => { closePreview(); btn.innerText = "🖨️ DRUCKEN"; }, 1000); } catch(e) { console.error(e); alert("Druck-Fehler: " + e.message); btn.innerText = "❌ FEHLER"; } }
window.onload = initCam;
</script>
</body>
</html>

118
public/apps/pixel.html Normal file
View File

@@ -0,0 +1,118 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Pixel Labor</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<script src="../js/tailwind.js"></script>
<style>
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
html { width: 100%; height: 100%; background-color: #0f172a; }
body {
position: fixed; top: 0; left: 0; right: 0; bottom: 0; margin: 0;
background-color: #0f172a;
color: white;
overflow: hidden !important;
user-select: none;
touch-action: none;
font-family: sans-serif;
animation: fadeIn 0.4s ease-out;
box-sizing: border-box;
padding-top: calc(10px + env(safe-area-inset-top));
padding-bottom: max(20px, env(safe-area-inset-bottom));
}
.pixel-grid { display: grid; grid-template-columns: repeat(16, 1fr); grid-template-rows: repeat(16, 1fr); gap: 1px; background: #334155; border: 8px solid #10b981; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.7); border-radius: 1rem; overflow: hidden; touch-action: none; width: 100%; height: 100%; aspect-ratio: 1 / 1; }
.cell { background-color: white; cursor: pointer; transition: background-color 0.05s; }
.no-scrollbar::-webkit-scrollbar { display: none; }
button:active { transform: scale(0.95); transition: transform 0.1s; }
@media print { body * { visibility: hidden; } #print-container, #print-container * { visibility: visible; } #print-container { position: fixed; left: 0; top: 0; width: 100%; height: 100%; display: flex !important; align-items: center; justify-content: center; background: white; z-index: 99999; } #print-img { max-width: 90%; max-height: 90%; object-fit: contain; border: 4px solid black; image-rendering: pixelated; } }
</style>
</head>
<body class="flex flex-col p-4"
onclick="if(window.resetIdleTimer) window.resetIdleTimer()"
ontouchstart="if(window.resetIdleTimer) window.resetIdleTimer()">
<div class="flex-none flex justify-between items-center shrink-0 mb-2 px-2 relative z-50 h-16">
<button onclick="goHome()" class="bg-slate-800 text-white border-4 border-slate-600 hover:border-white px-6 py-2 rounded-full text-lg font-black shadow-xl flex items-center gap-2 transition-transform active:scale-95 no-underline focus:outline-none shrink-0 z-50">
🏠 MENÜ
</button>
<div class="absolute inset-0 flex items-center justify-center pointer-events-none z-0">
<h1 class="text-2xl md:text-4xl font-black text-emerald-400 font-mono tracking-wider drop-shadow-[0_4px_4px_rgba(0,0,0,0.8)] uppercase">
&lt;PIXEL&gt;
</h1>
</div>
<button onclick="toggleInfo(); playSound('click')" class="w-12 h-12 bg-slate-800 text-white rounded-full font-bold border-4 border-slate-600 hover:border-white shadow-xl flex items-center justify-center transition-transform active:scale-95 text-xl shrink-0 z-50">
?
</button>
</div>
<div class="flex-1 flex flex-col lg:flex-row items-center justify-center gap-4 min-h-0 w-full max-w-[95rem] mx-auto pb-1">
<div class="relative w-full max-w-[650px] flex-shrink-0 flex items-center justify-center p-1 min-h-0 h-full">
<div id="grid" class="pixel-grid max-h-full"></div>
</div>
<div class="bg-slate-800 p-3 lg:p-4 rounded-[1.5rem] border-4 border-slate-700 flex flex-row lg:flex-col gap-3 shadow-2xl w-full lg:w-auto h-auto lg:h-full justify-between overflow-x-auto lg:overflow-visible lg:overflow-y-auto no-scrollbar">
<div class="grid grid-cols-4 lg:grid-cols-2 gap-2 flex-shrink-0 justify-items-center" id="palette">
<div class="color-btn w-12 h-12 lg:w-14 lg:h-14 rounded-xl bg-black border-4 border-slate-600 cursor-pointer active:scale-90 transition ring-4 ring-white shadow-lg" data-color="#000000" onclick="setColor(this)"></div>
<div class="color-btn w-12 h-12 lg:w-14 lg:h-14 rounded-xl bg-white border-4 border-slate-600 cursor-pointer active:scale-90 transition shadow-lg" data-color="#ffffff" onclick="setColor(this)"></div>
<div class="color-btn w-12 h-12 lg:w-14 lg:h-14 rounded-xl bg-red-500 border-4 border-slate-600 cursor-pointer active:scale-90 transition shadow-lg" data-color="#ef4444" onclick="setColor(this)"></div>
<div class="color-btn w-12 h-12 lg:w-14 lg:h-14 rounded-xl bg-blue-500 border-4 border-slate-600 cursor-pointer active:scale-90 transition shadow-lg" data-color="#3b82f6" onclick="setColor(this)"></div>
<div class="color-btn w-12 h-12 lg:w-14 lg:h-14 rounded-xl bg-green-500 border-4 border-slate-600 cursor-pointer active:scale-90 transition shadow-lg" data-color="#22c55e" onclick="setColor(this)"></div>
<div class="color-btn w-12 h-12 lg:w-14 lg:h-14 rounded-xl bg-yellow-400 border-4 border-slate-600 cursor-pointer active:scale-90 transition shadow-lg" data-color="#facc15" onclick="setColor(this)"></div>
<div class="color-btn w-12 h-12 lg:w-14 lg:h-14 rounded-xl bg-purple-500 border-4 border-slate-600 cursor-pointer active:scale-90 transition shadow-lg" data-color="#a855f7" onclick="setColor(this)"></div>
<div class="color-btn w-12 h-12 lg:w-14 lg:h-14 rounded-xl bg-slate-600 border-4 border-slate-500 cursor-pointer active:scale-90 transition flex items-center justify-center text-2xl shadow-lg" data-color="white" onclick="setColor(this)" title="Radierer">🧽</div>
</div>
<div class="h-1 w-full bg-slate-700 my-1 lg:block hidden rounded-full shrink-0"></div>
<div class="w-1 h-full bg-slate-700 mx-2 lg:hidden block rounded-full shrink-0"></div>
<div class="flex flex-col gap-2 w-full min-w-[160px] shrink-0">
<div class="flex gap-2">
<button onclick="downloadArt()" class="flex-1 bg-emerald-600 hover:bg-emerald-500 text-white py-3 px-2 rounded-xl font-black text-base shadow-lg active:translate-y-1 transition border-b-4 border-emerald-800 active:border-b-0 flex items-center justify-center gap-1">💾 SAVE</button>
<button id="btn-print" onclick="window.printArt(this)" class="flex-1 bg-white text-emerald-900 hover:bg-gray-200 py-3 px-2 rounded-xl font-black text-base shadow-lg active:translate-y-1 transition border-b-4 border-gray-300 active:border-b-0 flex items-center justify-center gap-1">🖨️ DRUCK</button>
</div>
<button onclick="clearGrid()" class="bg-slate-700 hover:bg-red-500/80 text-white py-3 px-4 rounded-xl font-bold transition text-base border-b-4 border-slate-900 active:border-b-0 active:translate-y-1 w-full flex items-center justify-center gap-2">🗑️ LÖSCHEN</button>
</div>
</div>
</div>
<div id="info-modal" class="hidden fixed inset-0 z-[200] bg-black/80 backdrop-blur-sm items-center justify-center p-4" onclick="toggleInfo()">
<div class="bg-slate-800 border-4 border-emerald-500 rounded-[2rem] max-w-3xl w-full p-8 shadow-2xl relative" onclick="event.stopPropagation()">
<button onclick="toggleInfo(); playSound('click')" class="absolute top-6 right-6 text-white hover:text-emerald-400 text-4xl font-bold transition"></button>
<h2 class="text-4xl font-black text-white mb-8 border-b border-slate-600 pb-4 flex items-center gap-4"><span class="text-6xl">👾</span> ANLEITUNG</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 text-base">
<div class="p-6 bg-slate-700/50 rounded-2xl border border-slate-600 flex flex-col items-center text-center"><div class="text-4xl mb-4">🎨</div><h3 class="text-emerald-400 font-bold text-xl mb-2">Malen</h3><p class="text-slate-300 leading-snug">Wähle rechts eine <b>Farbe</b> und tippe oder ziehe über die Quadrate.</p></div>
<div class="p-6 bg-slate-700/50 rounded-2xl border border-slate-600 flex flex-col items-center text-center"><div class="text-4xl mb-4">🧽</div><h3 class="text-white font-bold text-xl mb-2">Radieren</h3><p class="text-slate-300 leading-snug">Wähle den <b>Schwamm</b> oder drücke auf "LÖSCHEN", um von vorne zu beginnen.</p></div>
<div class="p-6 bg-slate-700/50 rounded-2xl border border-slate-600 flex flex-col items-center text-center"><div class="text-4xl mb-4">💾</div><h3 class="text-white font-bold text-xl mb-2">Fertig?</h3><p class="text-slate-300 leading-snug">Speichere dein Pixel-Kunstwerk oder drucke es direkt aus.</p></div>
</div>
<button onclick="toggleInfo(); playSound('click')" class="mt-8 w-full bg-emerald-600 hover:bg-emerald-500 text-white font-black py-4 rounded-xl text-xl transition">ALLES KLAR!</button>
</div>
</div>
<div id="print-container" class="hidden"><img id="print-img" src="" alt="Druckvorschau"></div>
<script>
const _soundCache = { 'click': new Audio('../assets/sounds/click.mp3'), 'shutter': new Audio('../assets/sounds/shutter.mp3'), 'success': new Audio('../assets/sounds/success.mp3') };
Object.values(_soundCache).forEach(s => s.load());
window.playSound = function(id) { if (_soundCache[id]) { const s = _soundCache[id].cloneNode(); s.volume = 1.0; s.play().catch(e => console.warn(e)); } };
function playPaintSound() {}
window.goHome = function() { playSound('click'); setTimeout(() => { window.location.href = '../index.html'; }, 300); };
const grid = document.getElementById('grid'); let currentColor = '#000000'; let isDrawing = false; let idleTimer;
function resetIdleTimer() { clearTimeout(idleTimer); idleTimer = setTimeout(() => { window.location.href = '../index.html'; }, 90000); }
['mousedown', 'touchstart', 'scroll', 'keydown', 'input'].forEach(evt => document.addEventListener(evt, resetIdleTimer, {passive: true})); resetIdleTimer();
function toggleInfo() { const m = document.getElementById('info-modal'); m.classList.toggle('hidden'); m.classList.toggle('flex'); }
function initGrid() { grid.innerHTML = ''; for(let i=0; i<256; i++) { const cell = document.createElement('div'); cell.className = 'cell'; cell.addEventListener('mousedown', () => { isDrawing = true; cell.style.backgroundColor = currentColor; playPaintSound(); }); cell.addEventListener('mouseenter', () => { if(isDrawing) { cell.style.backgroundColor = currentColor; } }); cell.addEventListener('touchstart', (e) => { isDrawing = true; cell.style.backgroundColor = currentColor; playPaintSound(); }, {passive: false}); grid.appendChild(cell); } }
grid.addEventListener('touchmove', (e) => { e.preventDefault(); if(!isDrawing) return; const t = e.touches[0]; const el = document.elementFromPoint(t.clientX, t.clientY); if(el && el.classList.contains('cell')) { el.style.backgroundColor = currentColor; } }, {passive: false});
document.body.addEventListener('mouseup', () => isDrawing = false); document.body.addEventListener('touchend', () => isDrawing = false);
function setColor(btn) { playSound('click'); currentColor = btn.getAttribute('data-color'); document.querySelectorAll('.color-btn').forEach(b => b.classList.remove('ring-4', 'ring-white')); btn.classList.add('ring-4', 'ring-white'); }
function clearGrid() { playSound('click'); if(confirm("Wirklich alles löschen?")) { document.querySelectorAll('.cell').forEach(c => c.style.backgroundColor = 'white'); playSound('shutter'); } }
function generateCanvas() { const c = document.createElement('canvas'); const ctx = c.getContext('2d'); const s = 32; c.width = 16 * s; c.height = 16 * s; document.querySelectorAll('.cell').forEach((cell, i) => { const x = (i % 16) * s; const y = Math.floor(i / 16) * s; ctx.fillStyle = cell.style.backgroundColor || 'white'; ctx.fillRect(x, y, s, s); }); return c; }
function downloadArt() { playSound('success'); const c = generateCanvas(); const l = document.createElement('a'); l.download = 'mein-pixel-art.png'; l.href = c.toDataURL(); l.click(); }
window.printArt = async function(el) { playSound('click'); const btn = el || document.getElementById('btn-print'); let oldText = ""; if(btn) { oldText = btn.innerHTML; btn.innerText = "⏳ Sende..."; btn.disabled = true; } const c = generateCanvas(); const d = c.toDataURL(); try { if (window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.DirectPrinter) { await window.Capacitor.Plugins.DirectPrinter.printBase64({ data: d, name: 'Pixel-Art' }); if(btn) btn.innerText = "✅ Gedruckt!"; playSound('success'); } else { const img = document.getElementById('print-img'); img.src = d; setTimeout(() => { window.print(); }, 100); if(btn) btn.innerText = "✅ OK"; playSound('success'); } } catch (e) { alert("Fehler: " + e.message); if(btn) btn.innerText = "❌ Fehler"; } setTimeout(() => { if(btn) { btn.innerHTML = oldText || '🖨️ <span class="hidden xl:inline">DRUCK</span>'; btn.disabled = false; } }, 3000); }
window.onload = initGrid;
</script>
</body>
</html>

126
public/apps/rec.html Normal file
View File

@@ -0,0 +1,126 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Mikro Check</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<script src="../js/tailwind.js"></script>
<style>
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
html { width: 100%; height: 100%; background-color: #0f172a; }
body {
position: fixed; top: 0; left: 0; right: 0; bottom: 0; margin: 0;
background-color: #0f172a;
color: white;
overflow: hidden;
user-select: none;
touch-action: manipulation;
animation: fadeIn 0.4s ease-out;
box-sizing: border-box;
padding-top: calc(10px + env(safe-area-inset-top));
padding-bottom: max(20px, env(safe-area-inset-bottom));
}
.no-scrollbar::-webkit-scrollbar { display: none; }
button:active { transform: scale(0.95); transition: transform 0.1s; }
.rec-active { animation: hardPulse 0.8s cubic-bezier(0.4, 0, 0.6, 1) infinite; background-color: #ef4444 !important; border-color: white !important; box-shadow: 0 0 30px rgba(239, 68, 68, 0.6) !important; }
@keyframes hardPulse { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.1); opacity: 0.9; } }
.playing { box-shadow: 0 0 20px #22c55e; border-color: #86efac !important; }
.effect-active { ring: 4px solid white; transform: scale(1.1); box-shadow: 0 0 15px rgba(255,255,255,0.4); z-index: 10; }
</style>
</head>
<body class="flex flex-col px-4"
onclick="if(window.resetIdleTimer) window.resetIdleTimer()"
ontouchstart="if(window.resetIdleTimer) window.resetIdleTimer()">
<div class="flex justify-between items-center mb-4 z-50 flex-none h-16">
<button onclick="goHome()" class="bg-slate-800 border-4 border-slate-600 hover:border-white px-4 py-2 rounded-full text-lg font-black shadow-xl flex items-center gap-2 transition-transform active:scale-95 text-white">
🏠 MENÜ
</button>
<button onclick="toggleInfo(); playSound('click')" class="w-12 h-12 bg-slate-800 text-white rounded-full font-bold border-4 border-slate-600 hover:border-white shadow-xl flex items-center justify-center text-xl">
?
</button>
</div>
<div class="flex-1 flex flex-col gap-4 relative w-full max-w-5xl mx-auto min-h-0">
<div class="relative w-full h-32 flex-none bg-slate-800 rounded-3xl border-4 border-slate-700 shadow-inner flex items-center justify-center overflow-hidden">
<canvas id="visualizer" class="w-full h-full"></canvas>
<div id="instruction-text" class="absolute text-slate-500 font-bold text-xl text-center px-4 pointer-events-none">
Mikrofon bereit... 🎤
</div>
</div>
<div class="flex-1 grid grid-cols-2 gap-4 min-h-0">
<div class="bg-slate-800 rounded-3xl border-4 border-slate-700 p-4 flex flex-col items-center justify-between shadow-lg relative overflow-hidden">
<div class="absolute top-0 left-0 bg-slate-700 px-4 py-1 rounded-br-xl font-black text-slate-400">SPUR 1</div>
<div id="timer-1" class="text-3xl md:text-4xl font-black font-mono text-slate-600 mt-2 transition-colors">00:00</div>
<div class="flex flex-col items-center gap-3 w-full">
<button id="btn-rec-1" onclick="toggleRecording(1)" class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-slate-700 border-8 border-slate-600 shadow-xl flex items-center justify-center transition-all hover:bg-slate-600 group">
<div id="icon-rec-1" class="w-6 h-6 md:w-8 md:h-8 bg-red-500 rounded-full shadow-lg group-hover:scale-110 transition-transform"></div>
</button>
<div id="fx-box-1" class="hidden flex gap-2 w-full justify-center mt-2">
<button onclick="setEffect(1, 0.6)" data-rate="0.6" class="fx-btn-1 w-12 h-12 rounded-xl bg-purple-600 text-2xl shadow-md border-b-4 border-purple-800 active:border-b-0 active:translate-y-1 transition flex items-center justify-center" title="Monster">🧟</button>
<button onclick="setEffect(1, 1)" data-rate="1" class="fx-btn-1 w-12 h-12 rounded-xl bg-blue-600 text-2xl shadow-md border-b-4 border-blue-800 active:border-b-0 active:translate-y-1 transition flex items-center justify-center" title="Normal">🙂</button>
<button onclick="setEffect(1, 1.7)" data-rate="1.7" class="fx-btn-1 w-12 h-12 rounded-xl bg-yellow-500 text-2xl shadow-md border-b-4 border-yellow-700 active:border-b-0 active:translate-y-1 transition flex items-center justify-center" title="Maus">🐭</button>
</div>
<button id="btn-play-1" onclick="playTrack(1)" class="hidden w-full bg-green-600 hover:bg-green-500 text-white font-black py-2 rounded-xl text-base shadow-lg border-b-4 border-green-800 active:border-b-0 active:translate-y-1 transition-all flex items-center justify-center gap-2"><span>▶️</span> ANHÖREN</button>
<button id="btn-del-1" onclick="deleteTrack(1)" class="hidden text-slate-500 hover:text-red-400 text-xs font-bold uppercase tracking-wider py-1">Löschen</button>
</div>
</div>
<div class="bg-slate-800 rounded-3xl border-4 border-slate-700 p-4 flex flex-col items-center justify-between shadow-lg relative overflow-hidden">
<div class="absolute top-0 left-0 bg-slate-700 px-4 py-1 rounded-br-xl font-black text-slate-400">SPUR 2</div>
<div id="timer-2" class="text-3xl md:text-4xl font-black font-mono text-slate-600 mt-2 transition-colors">00:00</div>
<div class="flex flex-col items-center gap-3 w-full">
<button id="btn-rec-2" onclick="toggleRecording(2)" class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-slate-700 border-8 border-slate-600 shadow-xl flex items-center justify-center transition-all hover:bg-slate-600 group">
<div id="icon-rec-2" class="w-6 h-6 md:w-8 md:h-8 bg-red-500 rounded-full shadow-lg group-hover:scale-110 transition-transform"></div>
</button>
<div id="fx-box-2" class="hidden flex gap-2 w-full justify-center mt-2">
<button onclick="setEffect(2, 0.6)" data-rate="0.6" class="fx-btn-2 w-12 h-12 rounded-xl bg-purple-600 text-2xl shadow-md border-b-4 border-purple-800 active:border-b-0 active:translate-y-1 transition flex items-center justify-center" title="Monster">🧟</button>
<button onclick="setEffect(2, 1)" data-rate="1" class="fx-btn-2 w-12 h-12 rounded-xl bg-blue-600 text-2xl shadow-md border-b-4 border-blue-800 active:border-b-0 active:translate-y-1 transition flex items-center justify-center" title="Normal">🙂</button>
<button onclick="setEffect(2, 1.7)" data-rate="1.7" class="fx-btn-2 w-12 h-12 rounded-xl bg-yellow-500 text-2xl shadow-md border-b-4 border-yellow-700 active:border-b-0 active:translate-y-1 transition flex items-center justify-center" title="Maus">🐭</button>
</div>
<button id="btn-play-2" onclick="playTrack(2)" class="hidden w-full bg-blue-600 hover:bg-blue-500 text-white font-black py-2 rounded-xl text-base shadow-lg border-b-4 border-blue-800 active:border-b-0 active:translate-y-1 transition-all flex items-center justify-center gap-2"><span>▶️</span> ANHÖREN</button>
<button id="btn-del-2" onclick="deleteTrack(2)" class="hidden text-slate-500 hover:text-red-400 text-xs font-bold uppercase tracking-wider py-1">Löschen</button>
</div>
</div>
</div>
</div>
<div id="info-modal" class="hidden fixed inset-0 z-[200] bg-black/80 backdrop-blur-sm items-center justify-center p-4" onclick="toggleInfo()">
<div class="bg-slate-800 border-4 border-red-500 rounded-[2rem] max-w-lg w-full p-8 shadow-2xl relative" onclick="event.stopPropagation()">
<button onclick="toggleInfo(); playSound('click')" class="absolute top-6 right-6 text-white hover:text-red-400 text-4xl font-bold transition"></button>
<h2 class="text-3xl font-black text-white mb-6 border-b border-slate-600 pb-4">🎙️ VOICE CHANGER</h2>
<ul class="text-slate-300 space-y-3 mb-8 list-disc pl-6 text-lg">
<li>Nimm links oder rechts etwas auf.</li>
<li>Drücke auf <b>Stopp</b>.</li>
<li>Jetzt erscheint Magie! ✨</li>
<li>Wähle: 🧟 <b>Monster</b> oder 🐭 <b>Maus</b>.</li>
<li>Hör es dir an!</li>
</ul>
<button onclick="toggleInfo(); playSound('click')" class="w-full bg-red-600 hover:bg-red-500 text-white font-black py-4 rounded-xl text-xl transition">ALLES KLAR!</button>
</div>
</div>
<script>
const _soundCache = {'click': new Audio('../assets/sounds/click.mp3'), 'shutter': new Audio('../assets/sounds/shutter.mp3'), 'success': new Audio('../assets/sounds/success.mp3')};
Object.values(_soundCache).forEach(s => s.load());
window.playSound = function(id) { if(_soundCache[id]) { const s = _soundCache[id].cloneNode(); s.volume = 1.0; s.play().catch(e=>{}); } };
window.goHome = function() { playSound('click'); setTimeout(() => { window.location.href = '../index.html'; }, 300); };
function toggleInfo() { const m = document.getElementById('info-modal'); m.classList.toggle('hidden'); m.classList.toggle('flex'); }
let micStream=null; let audioContext=null; let analyser=null; let dataArray=null; let gainNode=null;
const tracks = { 1: { isRecording: false, mediaRecorder: null, chunks: [], audio: new Audio(), hasData: false, timerInt: null, startTime: 0, playbackRate: 1, elementSource: null }, 2: { isRecording: false, mediaRecorder: null, chunks: [], audio: new Audio(), hasData: false, timerInt: null, startTime: 0, playbackRate: 1, elementSource: null } };
let idleTimer;
window.resetIdleTimer = function() { clearTimeout(idleTimer); const rec = tracks[1].isRecording || tracks[2].isRecording; if(!rec) { idleTimer = setTimeout(() => { window.location.href = '../index.html'; }, 120000); } };
['mousedown', 'touchstart', 'scroll', 'keydown', 'input'].forEach(evt => document.addEventListener(evt, window.resetIdleTimer, {passive: true})); window.resetIdleTimer();
async function initAudio() { try { if(!micStream) { micStream = await navigator.mediaDevices.getUserMedia({ audio: true }); audioContext = new (window.AudioContext || window.webkitAudioContext)(); gainNode = audioContext.createGain(); gainNode.gain.value = 1.5; gainNode.connect(audioContext.destination); analyser = audioContext.createAnalyser(); const s = audioContext.createMediaStreamSource(micStream); s.connect(analyser); analyser.fftSize = 256; dataArray = new Uint8Array(analyser.frequencyBinCount); drawVisualizer(); } if (audioContext && audioContext.state === 'suspended') await audioContext.resume(); } catch (e) { alert("Mikrofon Fehler: " + e.message); } }
function drawVisualizer() { const c = document.getElementById('visualizer'); if(!c) return; const ctx = c.getContext('2d'); c.width = c.offsetWidth; c.height = c.offsetHeight; function render() { requestAnimationFrame(render); if(!analyser) return; analyser.getByteFrequencyData(dataArray); ctx.fillStyle = '#1e293b'; ctx.fillRect(0, 0, c.width, c.height); const active = tracks[1].isRecording || tracks[2].isRecording; const alpha = active ? 1 : 0.3; let x = 0; const barW = (c.width / analyser.frequencyBinCount) * 2.5; for (let i = 0; i < analyser.frequencyBinCount; i++) { const h = dataArray[i] / 255 * c.height * 0.9; const r = active ? (h+50) : 50; const g = active ? 50 : (h+100); const b = active ? 50 : 200; ctx.fillStyle = `rgba(${r},${g},${b},${alpha})`; ctx.fillRect(x, c.height - h, barW, h); x += barW + 1; } } render(); }
async function toggleRecording(id) { await initAudio(); const t = tracks[id]; const btn = document.getElementById(`btn-rec-${id}`); const icon = document.getElementById(`icon-rec-${id}`); const playBtn = document.getElementById(`btn-play-${id}`); const delBtn = document.getElementById(`btn-del-${id}`); const fxBox = document.getElementById(`fx-box-${id}`); const timer = document.getElementById(`timer-${id}`); if (!t.isRecording) { playSound('shutter'); if(t.hasData) deleteTrack(id); t.mediaRecorder = new MediaRecorder(micStream); t.chunks = []; t.mediaRecorder.ondataavailable = e => t.chunks.push(e.data); t.mediaRecorder.onstop = () => { const b = new Blob(t.chunks, { type: 'audio/ogg; codecs=opus' }); t.audio.src = URL.createObjectURL(b); t.audio.preservesPitch = false; t.audio.mozPreservesPitch = false; t.audio.webkitPreservesPitch = false; t.hasData = true; playBtn.classList.remove('hidden'); delBtn.classList.remove('hidden'); fxBox.classList.remove('hidden'); fxBox.classList.add('flex'); setEffect(id, 1); playSound('success'); }; t.mediaRecorder.start(); t.isRecording = true; document.getElementById('instruction-text').classList.add('hidden'); btn.classList.add('rec-active'); icon.classList.replace('bg-red-500', 'bg-white'); timer.classList.replace('text-slate-600', 'text-white'); t.startTime = Date.now(); t.timerInt = setInterval(() => { const d = Math.floor((Date.now() - t.startTime) / 1000); timer.innerText = `${Math.floor(d/60).toString().padStart(2,'0')}:${(d%60).toString().padStart(2,'0')}`; }, 1000); } else { t.mediaRecorder.stop(); t.isRecording = false; clearInterval(t.timerInt); btn.classList.remove('rec-active'); icon.classList.replace('bg-white', 'bg-red-500'); timer.classList.replace('text-white', 'text-slate-600'); } }
function setEffect(id, rate) { playSound('click'); tracks[id].playbackRate = rate; document.querySelectorAll(`.fx-btn-${id}`).forEach(b => { b.classList.remove('effect-active', 'ring-4', 'ring-white'); if(b.getAttribute('data-rate') === rate.toString()) b.classList.add('effect-active', 'ring-4', 'ring-white'); }); }
function playTrack(id) { const t = tracks[id]; if(!t.hasData) return; playSound('click'); const btn = document.getElementById(`btn-play-${id}`); if (audioContext && !t.elementSource) { t.elementSource = audioContext.createMediaElementSource(t.audio); t.elementSource.connect(gainNode); } if (!t.audio.paused) { t.audio.pause(); t.audio.currentTime = 0; btn.classList.remove('playing'); btn.innerHTML = "<span>▶️</span> ANHÖREN"; } else { t.audio.playbackRate = t.playbackRate; t.audio.preservesPitch = false; if (audioContext.state === 'suspended') audioContext.resume(); t.audio.play(); btn.classList.add('playing'); btn.innerHTML = "<span>⏹</span> STOP"; t.audio.onended = () => { btn.classList.remove('playing'); btn.innerHTML = "<span>▶️</span> ANHÖREN"; }; } }
function deleteTrack(id) { playSound('click'); const t = tracks[id]; t.audio.pause(); t.chunks = []; t.hasData = false; document.getElementById(`btn-play-${id}`).classList.add('hidden'); document.getElementById(`btn-del-${id}`).classList.add('hidden'); document.getElementById(`fx-box-${id}`).classList.add('hidden'); document.getElementById(`fx-box-${id}`).classList.remove('flex'); document.getElementById(`timer-${id}`).innerText = "00:00"; }
</script>
</body>
</html>

161
public/apps/sound.html Normal file
View File

@@ -0,0 +1,161 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Sound Labor</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<script src="../js/tailwind.js"></script>
<style>
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
html {
width: 100%; height: 100%;
background-color: #0f172a;
}
body {
/* Fixiertes Layout */
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
margin: 0;
background-color: #0f172a; /* Dunkelblau */
color: white;
overflow: hidden !important;
user-select: none;
touch-action: manipulation;
font-family: sans-serif;
animation: fadeIn 0.4s ease-out;
box-sizing: border-box;
/* SAFE AREA PADDING */
padding-top: calc(10px + env(safe-area-inset-top));
padding-bottom: max(20px, env(safe-area-inset-bottom));
}
#pad-container {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(3, 1fr);
gap: 0.5rem; /* Etwas enger für mehr Platz */
width: 100%;
height: 100%;
}
.pad-item {
background-color: #1e293b;
border-radius: 1rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
border-bottom: 6px solid #0f172a;
position: relative;
user-select: none;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
transition: all 0.1s;
}
.pad-item:active { border-bottom-width: 0; transform: translateY(4px); }
button:active, a:active { transform: scale(0.95); transition: transform 0.1s; }
.pulse-rec { animation: pulseRed 1s infinite; border-color: #ef4444 !important; box-shadow: 0 0 25px #ef4444; }
@keyframes pulseRed { 0% { opacity: 1; } 50% { opacity: 0.6; } 100% { opacity: 1; } }
.fx-active { ring: 4px solid white; transform: scale(1.1); box-shadow: 0 0 15px rgba(255,255,255,0.5); }
.no-scrollbar::-webkit-scrollbar { display: none; }
</style>
</head>
<body class="flex flex-col p-2 md:p-4"
onclick="if(window.resetIdleTimer) window.resetIdleTimer()"
ontouchstart="if(window.resetIdleTimer) window.resetIdleTimer()">
<div class="flex-none flex justify-between items-center z-50 mb-2 relative h-16">
<button onclick="goHome()" class="bg-slate-800 text-white border-4 border-slate-600 hover:border-white px-4 py-2 rounded-full text-lg font-black shadow-xl flex items-center gap-2 transition-transform active:scale-95 no-underline focus:outline-none shrink-0 z-50">
🏠 MENÜ
</button>
<div class="absolute inset-0 flex items-center justify-center pointer-events-none z-0">
<h1 class="text-2xl md:text-4xl font-black text-white drop-shadow-[0_4px_4px_rgba(0,0,0,0.8)] tracking-widest uppercase">
✨ SOUND
</h1>
</div>
<button onclick="toggleInfo(); playSound('click')" class="w-12 h-12 bg-slate-800 text-white rounded-full font-bold border-4 border-slate-600 hover:border-white shadow-xl flex items-center justify-center transition-transform active:scale-95 text-xl shrink-0 z-50">
?
</button>
</div>
<div class="flex flex-col xl:flex-row justify-center items-center gap-2 z-40 mb-2 shrink-0">
<div id="visualizer-box" class="hidden relative w-48 h-12 bg-slate-800 rounded-xl border-2 border-red-500 overflow-hidden shadow-[0_0_20px_rgba(239,68,68,0.5)]">
<canvas id="visualizer" class="w-full h-full"></canvas>
<div class="absolute inset-0 flex items-center justify-center text-red-500 font-black tracking-widest text-[10px] pointer-events-none animate-pulse">● REC</div>
</div>
<div class="flex gap-2 bg-slate-800/50 p-1 rounded-xl border border-slate-700 items-center">
<button onclick="setGlobalFx(0.6, this)" class="fx-btn w-10 h-10 rounded-lg bg-purple-600 text-xl shadow-md border-b-2 border-purple-800 active:border-b-0 active:translate-y-1 transition" title="Monster">🧟</button>
<button onclick="setGlobalFx(1.0, this)" class="fx-btn w-10 h-10 rounded-lg bg-blue-600 text-xl shadow-md border-b-2 border-blue-800 active:border-b-0 active:translate-y-1 transition fx-active ring-4 ring-white" title="Normal">🙂</button>
<button onclick="setGlobalFx(1.7, this)" class="fx-btn w-10 h-10 rounded-lg bg-yellow-500 text-xl shadow-md border-b-2 border-yellow-700 active:border-b-0 active:translate-y-1 transition" title="Maus">🐭</button>
</div>
<div class="flex gap-2 bg-slate-800 p-1 rounded-2xl border-2 border-slate-700 shadow-xl w-full xl:w-auto justify-center">
<button id="btn-rec" onclick="setMode('rec')" class="flex items-center justify-center gap-2 px-4 py-2 rounded-xl font-black text-base transition-all border-2 border-transparent bg-slate-700 hover:bg-slate-600 text-gray-300 active:scale-95">
<div class="w-3 h-3 rounded-full bg-red-500 shadow-sm"></div> REC
</button>
<button id="btn-loop" onclick="setMode('loop')" class="flex items-center justify-center gap-2 px-4 py-2 rounded-xl font-black text-base transition-all border-2 border-transparent bg-slate-700 hover:bg-slate-600 text-gray-300 active:scale-95">
🔁 LOOP
</button>
<button onclick="stopAll(); playSound('click')" class="px-4 py-2 rounded-xl font-black text-base bg-slate-700 hover:bg-red-500 hover:text-white transition-all text-gray-300 border-2 border-transparent active:scale-95">
⏹ STOP
</button>
</div>
</div>
<div id="status-bar" class="text-center text-slate-400 mb-1 font-mono text-sm uppercase tracking-widest font-bold transition-all duration-300 shrink-0">
Modus: SPIELEN
</div>
<div id="pad-container" class="flex-1 min-h-0 w-full max-w-5xl mx-auto z-10 pb-1"></div>
<div id="info-modal" class="hidden fixed inset-0 z-[200] bg-black/80 backdrop-blur-sm items-center justify-center p-4" onclick="toggleInfo()">
<div class="bg-slate-800 border-4 border-rose-500 rounded-[2rem] max-w-3xl w-full p-8 shadow-2xl relative" onclick="event.stopPropagation()">
<button onclick="toggleInfo(); playSound('click')" class="absolute top-6 right-6 text-white hover:text-rose-400 text-4xl font-bold transition"></button>
<h2 class="text-4xl font-black text-white mb-8 border-b border-slate-600 pb-4 flex items-center gap-4"><span class="text-6xl">🎹</span> ANLEITUNG</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 text-base">
<div class="p-6 bg-slate-700/50 rounded-2xl border border-slate-600 flex flex-col items-center text-center"><div class="text-4xl mb-4">🧟</div><h3 class="text-purple-400 font-bold text-xl mb-2">Effekte</h3><p class="text-slate-300 leading-snug">Wähle <b>VOR</b> der Aufnahme oben Monster oder Maus.</p></div>
<div class="p-6 bg-slate-700/50 rounded-2xl border border-slate-600 flex flex-col items-center text-center"><div class="text-4xl mb-4">🎤</div><h3 class="text-rose-400 font-bold text-xl mb-2">Aufnehmen</h3><p class="text-slate-300 leading-snug">1. Tippe <b>REC</b>.<br>2. Wähle Pad.<br>3. Sprich.<br>4. Tippe Pad zum Stop.</p></div>
<div class="p-6 bg-slate-700/50 rounded-2xl border border-slate-600 flex flex-col items-center text-center"><div class="text-4xl mb-4">🔁</div><h3 class="text-blue-400 font-bold text-xl mb-2">Loopen</h3><p class="text-slate-300 leading-snug">Tippe <b>LOOP</b> und wähle Pads für Dauerschleife.</p></div>
</div>
<button onclick="toggleInfo(); playSound('click')" class="mt-8 w-full bg-rose-600 hover:bg-rose-500 text-white font-black py-4 rounded-xl text-xl transition">ALLES KLAR!</button>
</div>
</div>
<script>
const _soundCache = {'click': new Audio('../assets/sounds/click.mp3'), 'shutter': new Audio('../assets/sounds/shutter.mp3'), 'success': new Audio('../assets/sounds/success.mp3')};
Object.values(_soundCache).forEach(s => s.load());
window.playSound = function(id) { if(_soundCache[id]) { const s = _soundCache[id].cloneNode(); s.volume = 1.0; s.play().catch(e=>{}); } };
function goHome() { playSound('click'); setTimeout(() => { window.location.href = '../index.html'; }, 300); }
function toggleInfo() { const m = document.getElementById('info-modal'); m.classList.toggle('hidden'); m.classList.toggle('flex'); }
let idleTimer;
function resetIdleTimer() { clearTimeout(idleTimer); idleTimer = setTimeout(() => { if (typeof pads !== 'undefined' && Array.isArray(pads) && !pads.some(p => p.isRecording)) window.location.href = '../index.html'; }, 120000); }
['mousedown', 'touchstart', 'scroll', 'keydown', 'input'].forEach(evt => document.addEventListener(evt, resetIdleTimer, {passive: true})); resetIdleTimer();
let currentRecRate = 1.0;
function setGlobalFx(rate, btn) { playSound('click'); currentRecRate = rate; document.querySelectorAll('.fx-btn').forEach(b => b.classList.remove('fx-active', 'ring-4', 'ring-white')); btn.classList.add('fx-active', 'ring-4', 'ring-white'); }
let audioCtx; let pads = []; let currentMode = 'play'; let analyser, dataArray, visualizerFrame;
function init() { try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch(e) {} const container = document.getElementById('pad-container'); if(!container) return; container.innerHTML = ''; for(let i = 0; i < 12; i++) { pads.push({ buffer: null, loop: false, sourceNode: null, isRecording: false, mediaRecorder: null, chunks: [], playbackRate: 1.0 }); const el = document.createElement('div'); el.id = `pad-${i}`; el.className = `pad-item transition-all duration-100`; el.innerHTML = `<div class="text-4xl md:text-6xl font-black text-slate-700 transition-colors duration-300" id="label-${i}">${i+1}</div><div id="icon-loop-${i}" class="hidden absolute top-2 right-2 text-xl bg-blue-600 text-white rounded px-1 shadow-md">🔁</div><div id="icon-fx-${i}" class="absolute bottom-2 right-2 text-xl drop-shadow-md"></div><div id="icon-rec-${i}" class="hidden absolute top-3 left-3 w-4 h-4 bg-red-500 rounded-full animate-pulse shadow-[0_0_10px_red]"></div><div id="status-${i}" class="hidden absolute bottom-2 w-full text-center text-xs font-black uppercase tracking-widest text-emerald-400">BELEGT</div>`; el.onpointerdown = (e) => { e.preventDefault(); handlePadTouch(i); }; container.appendChild(el); } }
window.setMode = function(mode) { playSound('click'); if (currentMode === mode) currentMode = 'play'; else currentMode = mode; updateUI(); }
function updateUI() { const btnRec = document.getElementById('btn-rec'); const btnLoop = document.getElementById('btn-loop'); const status = document.getElementById('status-bar'); const inactive = "border-transparent bg-slate-700 text-gray-300 hover:bg-slate-600"; const activeRec = "border-red-500 text-red-400 bg-slate-900 shadow-[0_0_10px_rgba(239,68,68,0.2)]"; const activeLoop = "border-blue-500 text-blue-400 bg-slate-900 shadow-[0_0_10px_rgba(59,130,246,0.2)]"; const base = "flex items-center justify-center gap-2 px-4 py-2 rounded-xl font-black text-base transition-all border-2 active:scale-95 "; btnRec.className = base + (currentMode === 'rec' ? activeRec : inactive); btnLoop.className = base + (currentMode === 'loop' ? activeLoop : inactive); if (currentMode === 'rec') { status.innerText = "🔴 AUFNAHME"; status.className = "text-center mb-1 font-mono text-sm uppercase tracking-widest text-red-400 animate-pulse font-bold"; } else if (currentMode === 'loop') { status.innerText = "🔁 LOOP SETZEN"; status.className = "text-center mb-1 font-mono text-sm uppercase tracking-widest text-blue-400 font-bold"; } else { status.innerText = "Modus: SPIELEN"; status.className = "text-center text-slate-400 mb-1 font-mono text-sm uppercase tracking-widest font-bold"; } }
async function handlePadTouch(i) { resetIdleTimer(); if (audioCtx && audioCtx.state === 'suspended') await audioCtx.resume(); const pad = pads[i]; const el = document.getElementById(`pad-${i}`); if (currentMode === 'play') { if (!pad.buffer) return; if (pad.loop) { if (pad.sourceNode) { pad.sourceNode.stop(); pad.sourceNode = null; el.classList.remove('bg-blue-600', 'border-blue-800'); el.style.backgroundColor = "#1e293b"; } else { playMusicPad(i, true); el.style.backgroundColor = "#2563eb"; } } else { playMusicPad(i, false); el.style.backgroundColor = "#10b981"; setTimeout(() => el.style.backgroundColor = "#1e293b", 150); } } else if (currentMode === 'loop') { playSound('click'); if (!pad.buffer) return; pad.loop = !pad.loop; document.getElementById(`icon-loop-${i}`).classList.toggle('hidden', !pad.loop); el.style.transform = "scale(0.95)"; setTimeout(() => el.style.transform = "scale(1)", 100); } else if (currentMode === 'rec') { if (pad.isRecording) stopRecording(i); else startRecording(i); } }
function playMusicPad(i, loop) { const pad = pads[i]; if (pad.sourceNode) { try { pad.sourceNode.stop(); } catch(e){} pad.sourceNode = null; } const source = audioCtx.createBufferSource(); source.buffer = pad.buffer; source.loop = loop; source.playbackRate.value = pad.playbackRate; source.connect(audioCtx.destination); source.start(0); if (loop) pad.sourceNode = source; }
function startVisualizer(stream) { document.getElementById('visualizer-box').classList.remove('hidden'); const canvas = document.getElementById('visualizer'); const ctx = canvas.getContext('2d'); const source = audioCtx.createMediaStreamSource(stream); analyser = audioCtx.createAnalyser(); source.connect(analyser); analyser.fftSize = 256; dataArray = new Uint8Array(analyser.frequencyBinCount); function draw() { visualizerFrame = requestAnimationFrame(draw); analyser.getByteFrequencyData(dataArray); canvas.width = canvas.offsetWidth; canvas.height = canvas.offsetHeight; ctx.fillStyle = '#1e293b'; ctx.fillRect(0, 0, canvas.width, canvas.height); const barWidth = (canvas.width / analyser.frequencyBinCount) * 2.5; let x = 0; for(let i = 0; i < analyser.frequencyBinCount; i++) { const barHeight = dataArray[i] / 255 * canvas.height; ctx.fillStyle = `rgb(${barHeight+50},50,50)`; ctx.fillRect(x, canvas.height - barHeight, barWidth, barHeight); x += barWidth + 1; } } draw(); }
function stopVisualizer() { document.getElementById('visualizer-box').classList.add('hidden'); cancelAnimationFrame(visualizerFrame); }
async function startRecording(i) { playSound('shutter'); const pad = pads[i]; const el = document.getElementById(`pad-${i}`); try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); startVisualizer(stream); pad.mediaRecorder = new MediaRecorder(stream); pad.chunks = []; pad.mediaRecorder.ondataavailable = (e) => pad.chunks.push(e.data); pad.mediaRecorder.onstop = async () => { const blob = new Blob(pad.chunks, { 'type' : 'audio/webm; codecs=opus' }); const arrayBuffer = await blob.arrayBuffer(); audioCtx.decodeAudioData(arrayBuffer, (decodedBuffer) => { pad.buffer = decodedBuffer; pad.playbackRate = currentRecRate; updatePadVisuals(i, true); }, (e) => console.error(e)); stream.getTracks().forEach(track => track.stop()); stopVisualizer(); }; pad.mediaRecorder.start(); pad.isRecording = true; document.getElementById(`icon-rec-${i}`).classList.remove('hidden'); el.classList.add('pulse-rec'); } catch (err) { alert("Mikrofon Fehler: " + err.message); } }
function stopRecording(i) { playSound('success'); const pad = pads[i]; const el = document.getElementById(`pad-${i}`); if (pad.mediaRecorder && pad.mediaRecorder.state !== 'inactive') pad.mediaRecorder.stop(); pad.isRecording = false; document.getElementById(`icon-rec-${i}`).classList.add('hidden'); el.classList.remove('pulse-rec'); window.setMode('play'); }
function updatePadVisuals(i, hasSound) { const el = document.getElementById(`pad-${i}`); const label = document.getElementById(`label-${i}`); const status = document.getElementById(`status-${i}`); const iconFx = document.getElementById(`icon-fx-${i}`); const pad = pads[i]; if (hasSound) { el.style.borderColor = "#10b981"; label.classList.remove('text-slate-700'); label.classList.add('text-white'); status.classList.remove('hidden'); if(pad.playbackRate < 0.9) iconFx.innerText = "🧟"; else if(pad.playbackRate > 1.2) iconFx.innerText = "🐭"; else iconFx.innerText = ""; } }
window.stopAll = function() { if(pads && pads.length > 0) { pads.forEach((pad, i) => { if (pad.sourceNode) { try { pad.sourceNode.stop(); } catch(e){} pad.sourceNode = null; } const el = document.getElementById(`pad-${i}`); if(el) { el.style.backgroundColor = "#1e293b"; el.classList.remove('bg-blue-600'); } }); } }
init();
</script>
</body>
</html>

BIN
public/assets/dino.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 677 KiB

BIN
public/assets/dschungel.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
public/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
public/assets/news.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

BIN
public/assets/ozean.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 KiB

BIN
public/assets/paris.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
public/assets/qr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
public/assets/schloss.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 KiB

Binary file not shown.

Binary file not shown.

BIN
public/assets/sounds/brick.mp3 Executable file

Binary file not shown.

Binary file not shown.

BIN
public/assets/sounds/fail.mp3 Executable file

Binary file not shown.

BIN
public/assets/sounds/paddle.mp3 Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/assets/sounds/wall.mp3 Executable file

Binary file not shown.

BIN
public/assets/stadion.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 875 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
public/assets/weltraum.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
public/assets/wolken.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

1559
public/cordova.js vendored Normal file

File diff suppressed because it is too large Load Diff

36
public/cordova_plugins.js vendored Normal file
View File

@@ -0,0 +1,36 @@
cordova.define('cordova/plugin_list', function(require, exports, module) {
module.exports = [
{
"id": "cordova-plugin-printer.Printer",
"file": "plugins/cordova-plugin-printer/www/printer.js",
"pluginId": "cordova-plugin-printer",
"clobbers": [
"cordova.plugins.printer"
]
},
{
"id": "cordova-plugin-x-socialsharing.SocialSharing",
"file": "plugins/cordova-plugin-x-socialsharing/www/SocialSharing.js",
"pluginId": "cordova-plugin-x-socialsharing",
"clobbers": [
"window.plugins.socialsharing"
]
},
{
"id": "es6-promise-plugin.Promise",
"file": "plugins/es6-promise-plugin/www/promise.js",
"pluginId": "es6-promise-plugin",
"runs": true
}
];
module.exports.metadata =
// TOP OF METADATA
{
"cordova-plugin-printer": "0.8.0",
"cordova-plugin-x-socialsharing": "6.0.4",
"es6-promise-plugin": "4.2.2"
};
// BOTTOM OF METADATA
});

270
public/index.html Normal file
View File

@@ -0,0 +1,270 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>MedienStation Hub</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<script src="js/tailwind.js"></script>
<style>
/* Globaler Reset */
html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; }
body {
background-color: #0f172a;
color: white;
font-family: sans-serif;
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
}
.font-comic { font-family: 'Comic Sans MS', 'Chalkboard SE', sans-serif; }
.app-card { cursor: pointer; transition: transform 0.1s; }
.app-card:active { transform: scale(0.95); filter: brightness(1.2); }
.no-scrollbar::-webkit-scrollbar { display: none; }
/* Animationen */
.fade-in { animation: fadeIn 0.5s ease-out; }
@keyframes fadeIn { from { opacity: 0; transform: scale(0.98); } to { opacity: 1; transform: scale(1); } }
/* Grid & Ghost Mode für QR Modal */
#qr-modal {
display: grid !important;
place-items: center;
position: fixed;
top: 0; left: 0; width: 100vw; height: 100vh;
z-index: 99999;
background-color: rgba(0,0,0,0.85);
backdrop-filter: blur(5px);
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease-in-out;
}
#qr-modal.modal-visible {
opacity: 1;
pointer-events: auto;
}
</style>
</head>
<body class="p-4 pt-8 box-border fade-in relative" onpointerdown="resetHubTimer()" ontouchstart="resetHubTimer()">
<div class="grid grid-cols-12 grid-rows-12 gap-3 h-full w-full pb-20">
<div class="col-span-12 row-span-2 bg-slate-800 rounded-3xl px-6 flex justify-between items-center shadow-lg border border-slate-700 relative overflow-hidden group gap-4">
<div class="absolute -right-10 -top-10 w-64 h-64 bg-yellow-400 rounded-full blur-3xl opacity-10 group-hover:opacity-20 transition-opacity duration-500 pointer-events-none"></div>
<div class="z-10 flex-1 select-none cursor-pointer hover:opacity-80 transition-opacity flex flex-col justify-center" onclick="triggerAdmin()">
<h3 class="text-slate-400 text-xs font-bold tracking-[0.2em] uppercase mb-1">DEINE MISSION</h3>
<div id="mission-text" class="text-xl md:text-3xl lg:text-4xl font-black text-white leading-tight truncate">Wähle eine App! 👉</div>
</div>
<button onclick="playSound('click'); newMission()" class="shrink-0 z-10 bg-yellow-400 hover:bg-yellow-300 text-black font-black text-lg py-3 px-8 rounded-full shadow-lg active:translate-y-1 transition-all transform hover:scale-105 flex items-center gap-2">
<span class="text-2xl">🎲</span> <span class="hidden md:inline">NEU</span>
</button>
</div>
<div onclick="openApp('apps/sound.html', 'Sound')" class="app-card col-span-3 row-span-5 bg-gradient-to-br from-rose-500 to-rose-600 rounded-3xl flex flex-col items-center justify-center shadow-lg border-b-8 border-rose-800 active:border-b-0 group">
<div class="text-6xl mb-2 drop-shadow-md group-hover:-translate-y-2 transition-transform">🎹</div>
<h3 class="text-2xl font-bold">Sound</h3>
</div>
<div onclick="openApp('apps/rec.html', 'Mikro')" class="app-card col-span-3 row-span-5 bg-gradient-to-br from-red-600 to-red-700 rounded-3xl flex flex-col items-center justify-center shadow-lg border-b-8 border-red-900 active:border-b-0">
<div class="text-6xl mb-2 drop-shadow-md">🎙️</div>
<h3 class="text-2xl font-bold">Mikro</h3>
</div>
<div onclick="openApp('apps/gif.html', 'Loop')" class="app-card col-span-3 row-span-5 bg-gradient-to-br from-sky-500 to-sky-600 rounded-3xl flex flex-col items-center justify-center shadow-lg border-b-8 border-sky-800 active:border-b-0 group">
<div class="text-6xl mb-2 drop-shadow-md group-hover:rotate-12 transition-transform">📹</div>
<h3 class="text-2xl font-bold">Loop</h3>
</div>
<div onclick="openApp('apps/magic.html', 'Magic')" class="app-card col-span-3 row-span-5 bg-gradient-to-br from-violet-500 to-violet-600 rounded-3xl flex flex-col items-center justify-center shadow-lg border-b-8 border-violet-800 active:border-b-0">
<div class="text-6xl mb-2 drop-shadow-md"></div>
<h3 class="text-2xl font-bold">Magic</h3>
</div>
<div onclick="openApp('apps/pixel.html', 'Pixel')" class="app-card col-span-3 row-span-5 bg-gradient-to-br from-emerald-500 to-emerald-700 rounded-3xl flex flex-col items-center justify-center shadow-lg border-b-8 border-emerald-900 active:border-b-0">
<div class="text-6xl mb-2 drop-shadow-md font-mono">👾</div>
<h3 class="text-2xl font-bold">Pixel</h3>
</div>
<div onclick="openApp('apps/news.html', 'News')" class="app-card col-span-3 row-span-5 bg-gradient-to-br from-blue-700 to-blue-900 rounded-3xl flex flex-col items-center justify-center shadow-lg border-b-8 border-blue-950 active:border-b-0 relative overflow-hidden">
<div class="text-6xl mb-2 drop-shadow-md relative z-10">📰</div>
<h3 class="text-2xl font-bold relative z-10">News</h3>
</div>
<div onclick="openApp('apps/comic.html', 'Comic')" class="app-card col-span-3 row-span-5 bg-gradient-to-r from-yellow-400 to-orange-400 rounded-3xl flex items-center justify-center shadow-lg border-b-8 border-orange-600 active:border-b-0 group gap-2">
<div class="text-5xl drop-shadow-md transform -rotate-6 transition-transform group-hover:rotate-0">📸</div>
<h3 class="text-2xl font-black font-comic tracking-wide text-black">Comic</h3>
</div>
<div onclick="toggleInfo(); playSound('click')" class="app-card col-span-3 row-span-5 bg-slate-700 hover:bg-slate-600 rounded-3xl flex flex-col items-center justify-center shadow-lg border-b-8 border-slate-900 active:border-b-0 border-2 border-slate-600">
<div class="text-6xl mb-2 drop-shadow-md">💡</div>
<h3 class="text-2xl font-bold text-slate-200">Infos</h3>
</div>
</div>
<div class="fixed bottom-6 w-full flex flex-col items-center justify-center pointer-events-none z-40">
<button onclick="toggleQR()" class="pointer-events-auto bg-slate-800/90 backdrop-blur border border-slate-600 px-8 py-3 rounded-full shadow-2xl flex items-center gap-4 hover:bg-slate-700 hover:scale-105 hover:border-white transition-all">
<img src="assets/logo.png" class="h-12 w-auto object-contain" alt="Logo" onerror="this.style.display='none'">
<div class="flex flex-col text-left">
<span class="text-[10px] text-slate-400 font-bold uppercase tracking-widest leading-none mb-1">Ein Projekt der</span>
<span class="text-lg font-black text-white leading-none tracking-wide">AV-MEDIENZENTRALE</span>
</div>
</button>
</div>
<div id="info-modal" class="hidden fixed inset-0 z-[200] bg-black/90 backdrop-blur-md flex items-center justify-center p-6">
<div class="bg-slate-800 border-2 border-slate-600 rounded-[2.5rem] w-full max-w-5xl p-8 shadow-2xl relative max-h-[90vh] overflow-y-auto no-scrollbar">
<button onclick="toggleInfo(); playSound('click')" class="absolute top-6 right-6 text-white text-5xl font-bold hover:text-slate-300 transition"></button>
<h2 class="text-4xl font-black text-white mb-6 border-b-2 border-slate-600 pb-2 tracking-wide uppercase">Apps & Funktionen</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-12">
<div class="p-4 bg-slate-700/50 rounded-2xl flex items-center gap-4 border border-slate-600/50"><div class="text-5xl shrink-0">🎹</div><div><h3 class="text-rose-400 font-black text-xl">Sound</h3><p class="text-slate-300">Baue eigene Beats & Songs.</p></div></div>
<div class="p-4 bg-slate-700/50 rounded-2xl flex items-center gap-4 border border-slate-600/50"><div class="text-5xl shrink-0">🎙️</div><div><h3 class="text-red-400 font-black text-xl">Mikro</h3><p class="text-slate-300">Verstelle deine Stimme (Monster/Maus).</p></div></div>
<div class="p-4 bg-slate-700/50 rounded-2xl flex items-center gap-4 border border-slate-600/50"><div class="text-5xl shrink-0">📹</div><div><h3 class="text-sky-400 font-black text-xl">Loop</h3><p class="text-slate-300">Drehe lustige Wackel-Videos.</p></div></div>
<div class="p-4 bg-slate-700/50 rounded-2xl flex items-center gap-4 border border-slate-600/50"><div class="text-5xl shrink-0"></div><div><h3 class="text-violet-400 font-black text-xl">Magic</h3><p class="text-slate-300">Green Screen: Beame dich weg!</p></div></div>
<div class="p-4 bg-slate-700/50 rounded-2xl flex items-center gap-4 border border-slate-600/50"><div class="text-5xl shrink-0">👾</div><div><h3 class="text-emerald-400 font-black text-xl">Pixel</h3><p class="text-slate-300">Male Retro-Kunstwerke.</p></div></div>
<div class="p-4 bg-slate-700/50 rounded-2xl flex items-center gap-4 border border-slate-600/50"><div class="text-5xl shrink-0">📰</div><div><h3 class="text-blue-400 font-black text-xl">News</h3><p class="text-slate-300">Werde Nachrichtensprecher.</p></div></div>
<div class="p-4 bg-slate-700/50 rounded-2xl flex items-center gap-4 border border-slate-600/50 md:col-span-2"><div class="text-5xl shrink-0">📸</div><div><h3 class="text-orange-400 font-black text-xl">Comic</h3><p class="text-slate-300">Erstelle deine eigene Foto-Story mit Sprechblasen.</p></div></div>
</div>
<h2 class="text-4xl font-black text-white mb-6 border-b-2 border-slate-600 pb-2 tracking-wide uppercase">Pädagogik & Konzept</h2>
<div class="space-y-6 text-slate-300">
<div>
<h4 class="text-2xl font-bold text-yellow-400 mb-2">🎯 Das Ziel</h4>
<p class="text-lg">Die MedienStation fördert spielerisch <b>Medienkompetenz</b>. Kinder produzieren selbst Medien (Bild, Ton, Video), statt sie nur zu konsumieren. Dabei lernen sie technische Grundlagen und hinterfragen Medieninhalte ("Fake News", Bildbearbeitung).</p>
</div>
<div>
<h4 class="text-2xl font-bold text-rose-400 mb-2">💡 Reflexions-Fragen für alle Apps</h4>
<ul class="list-disc pl-6 space-y-2 text-lg">
<li><b>🎹 Sound:</b> Wie verändert Musik die Stimmung eines Films? Würde ein Horrorfilm mit lustiger Musik noch gruselig wirken?</li>
<li><b>🎙️ Mikro:</b> Wie verändert eine tiefe oder hohe Stimme deine Wirkung auf andere? Fühlst du dich als "Monster" mutiger?</li>
<li><b>📹 Loop:</b> Warum wirken Loops oft lustig? Was passiert, wenn eine Bewegung nie aufhört?</li>
<li><b>✨ Magic:</b> Wirkte das Foto echt? Traust du Bildern im Internet jetzt noch genauso schnell?</li>
<li><b>👾 Pixel:</b> Wie viele Quadrate (Pixel) braucht man mindestens, um ein Gesicht zu erkennen?</li>
<li><b>📰 News:</b> Fühlt man sich wichtiger, wenn "LIVE" und "BREAKING NEWS" im Bild steht?</li>
<li><b>📸 Comic:</b> Erzählt ein Bild mehr als 1000 Worte? Wie verändert die Sprechblase die Bedeutung des Fotos?</li>
</ul>
</div>
</div>
</div>
</div>
<div id="qr-modal" onclick="toggleQR()">
<div onclick="event.stopPropagation()"
style="width: 90%; max-width: 400px; background: white; padding: 40px; border-radius: 30px; text-align: center; box-shadow: 0 20px 50px rgba(0,0,0,0.5);">
<h3 style="color: #0f172a; font-size: 24px; font-weight: 900; text-transform: uppercase; margin: 0 0 20px 0;">Besuche uns!</h3>
<div style="width: 250px; height: 250px; background: #f8fafc; border: 2px solid #e2e8f0; border-radius: 15px; margin: 0 auto 20px auto; display: flex; align-items: center; justify-content: center;">
<img src="assets/qr.png" alt="QR Code" style="width: 230px; height: 230px; object-fit: contain;"
onerror="this.src='data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iI2ZmZiIvPjx0ZXh0IHg9IjUwIiB5PSI1MCIgZm9udC1zaXplPSIxMCIgdGV4dC1hbmNob3I9Im1pZGRsZSI+UVIgQ29kZTwvdGV4dD48L3N2Zz4='">
</div>
<p style="color: #64748b; font-weight: bold; margin-bottom: 25px;">Scan den Code mit deinem Handy.</p>
<button onclick="toggleQR()"
style="width: 100%; background-color: #0f172a; color: white; border: none; padding: 15px; font-size: 18px; font-weight: 900; border-radius: 12px; cursor: pointer;">
SCHLIESSEN
</button>
</div>
</div>
<div id="admin-modal" class="hidden fixed inset-0 bg-black/90 z-[300] flex items-center justify-center p-4">
<div class="bg-slate-800 p-8 rounded-3xl border-2 border-slate-600 w-full max-w-lg">
<h2 class="text-3xl font-black text-white mb-6">⚙️ ADMIN MENU</h2>
<div class="grid grid-cols-2 gap-4">
<button onclick="location.reload()" class="bg-blue-600 text-white font-bold py-4 rounded-xl">🔄 Reload</button>
<button onclick="closeAdmin()" class="bg-slate-600 text-white font-bold py-4 rounded-xl">Zurück</button>
</div>
</div>
</div>
<script>
// --- MISSIONEN ---
const missions = [
// Standard
"Erstelle ein Geräusch: Kälte ❄️",
"Reise mit Magic Selfie auf den Mond 🚀",
"Werbung für 'Unsichtbare Socken' 🧦",
"Loop: 'Hallo' ohne Worte sagen 👋",
"Witz in 3 Comic-Bildern 🤡",
"Magic Selfie: Du fliegst durch die Luft ✨",
"Vertone einen Elefanten mit Schluckauf 🐘",
"Pixel-Monster Haustier malen 👾",
"News: Ufo gelandet! 👽",
"Interviewe dich selbst aus der Zukunft! 🎤",
"Erfinde einen geheimen Handschlag! 🤝",
"Mache ein Geräusch wie ein wütender Toaster! 🍞",
"Zeige deinen besten Roboter-Tanz 🤖",
"News: Hausaufgaben offiziell verboten! 🚫",
// Neue kreative Ideen
"Pixel-Art: Dein Traumhaus aus Süßigkeiten 🍭",
"Sound: Ein Beat nur aus Körpergeräuschen 👏",
"Comic: Der Tag, an dem die Schwerkraft ausfiel 🎈",
"News: Katzen übernehmen die Weltherrschaft! 😼",
"Loop: Ein Zaubertrick, der schiefgeht 🪄"
];
const playSound = (id) => {
const a = new Audio(`assets/sounds/${id}.mp3`);
a.volume = 1.0;
a.play().catch(() => {});
};
function openApp(url, appName) {
playSound('click');
setTimeout(() => { window.location.href = url; }, 300);
}
function newMission() {
const txt = document.getElementById('mission-text');
txt.innerText = missions[Math.floor(Math.random() * missions.length)];
}
let hubIdleTimer;
function resetHubTimer() {
clearTimeout(hubIdleTimer);
hubIdleTimer = setTimeout(() => {
document.getElementById('admin-modal').classList.add('hidden');
document.getElementById('info-modal').classList.add('hidden');
document.getElementById('qr-modal').classList.remove('modal-visible');
document.getElementById('mission-text').innerText = "Wähle eine App! 👉";
tapCount = 0;
}, 60000);
}
resetHubTimer();
function toggleInfo() {
resetHubTimer();
const m = document.getElementById('info-modal');
m.classList.toggle('hidden');
m.classList.toggle('flex');
}
function toggleQR() {
resetHubTimer();
const modal = document.getElementById('qr-modal');
modal.classList.toggle('modal-visible');
playSound('click');
}
let tapCount = 0; let tapTimer;
function triggerAdmin() {
resetHubTimer(); tapCount++; clearTimeout(tapTimer); tapTimer = setTimeout(() => { tapCount = 0; }, 1000);
if (tapCount >= 5) {
tapCount = 0;
if (prompt("PIN:") === "1234") {
document.getElementById('admin-modal').classList.remove('hidden');
document.getElementById('admin-modal').classList.add('flex');
}
}
}
function closeAdmin() {
document.getElementById('admin-modal').classList.add('hidden');
document.getElementById('admin-modal').classList.remove('flex');
}
</script>
</body>
</html>

87
public/js/audio.js Normal file
View File

@@ -0,0 +1,87 @@
(function() {
console.log("🔊 AUDIO SYSTEM GESTARTET");
// 1. Pfad bestimmen
const isSubApp = window.location.pathname.includes('/apps/');
// Wir probieren beide Pfade, falls einer falsch ist
const basePath = isSubApp ? '../assets/sounds/' : 'assets/sounds/';
// 2. Sounds laden mit Fehlerprüfung
const soundNames = ['click', 'shutter', 'success'];
const audioStore = {};
soundNames.forEach(name => {
const fullPath = basePath + name + '.mp3';
const audio = new Audio();
// Event Listener VOR dem src setzen
audio.addEventListener('canplaythrough', () => {
console.log(`✅ Sound geladen: ${name} (${fullPath})`);
});
audio.addEventListener('error', (e) => {
console.error(`❌ FEHLER bei Sound ${name}:`, e);
console.error(`Versuchter Pfad: ${fullPath}`);
// Versuch: Absoluter Pfad als Fallback für Android
if (!audio.src.startsWith('http') && !audio.src.startsWith('file')) {
console.log("Versuche Fallback-Pfad...");
}
});
audio.src = fullPath;
audio.load();
audioStore[name] = audio;
});
audioStore['click'].volume = 0.5;
audioStore['shutter'].volume = 1.0;
audioStore['success'].volume = 0.8;
// 3. Globale Play Funktion
window.playSound = function(name) {
const sound = audioStore[name];
if (sound) {
console.log(`▶️ Spiele: ${name}`);
const clone = sound.cloneNode();
clone.volume = sound.volume;
const promise = clone.play();
if (promise !== undefined) {
promise.then(() => {
console.log(`🔊 ${name} abgespielt!`);
}).catch(error => {
console.error(`🚫 Autoplay blockiert oder Fehler bei ${name}:`, error);
alert("Audio Fehler: " + error.message); // Damit du es am Tablet siehst
});
}
} else {
console.error(`❓ Unbekannter Sound: ${name}`);
}
};
// 4. Klick-Listener an alle Buttons
function initButtonSounds() {
const buttons = document.querySelectorAll('button, a, .pad-item, .pad-btn');
console.log(`Found ${buttons.length} clickable elements.`);
buttons.forEach(btn => {
if(btn.dataset.soundAttached) return;
btn.dataset.soundAttached = "true";
btn.addEventListener('touchstart', () => {
const txt = (btn.innerText || "").toUpperCase();
if (!txt.includes('FOTO') && !txt.includes('REC') && !txt.includes('DRUCKEN') && !txt.includes('WÜRFELN')) {
window.playSound('click');
}
}, {passive: true});
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initButtonSounds);
} else {
initButtonSounds();
}
window.refreshAudio = initButtonSounds;
})();

View File

@@ -0,0 +1,93 @@
(function(){/*
Copyright The Closure Library Authors.
SPDX-License-Identifier: Apache-2.0
*/
'use strict';var x;function aa(a){var b=0;return function(){return b<a.length?{done:!1,value:a[b++]}:{done:!0}}}var ba="function"==typeof Object.defineProperties?Object.defineProperty:function(a,b,c){if(a==Array.prototype||a==Object.prototype)return a;a[b]=c.value;return a};
function ca(a){a=["object"==typeof globalThis&&globalThis,a,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global];for(var b=0;b<a.length;++b){var c=a[b];if(c&&c.Math==Math)return c}throw Error("Cannot find global object");}var y=ca(this);function z(a,b){if(b)a:{var c=y;a=a.split(".");for(var d=0;d<a.length-1;d++){var e=a[d];if(!(e in c))break a;c=c[e]}a=a[a.length-1];d=c[a];b=b(d);b!=d&&null!=b&&ba(c,a,{configurable:!0,writable:!0,value:b})}}
z("Symbol",function(a){function b(g){if(this instanceof b)throw new TypeError("Symbol is not a constructor");return new c(d+(g||"")+"_"+e++,g)}function c(g,f){this.h=g;ba(this,"description",{configurable:!0,writable:!0,value:f})}if(a)return a;c.prototype.toString=function(){return this.h};var d="jscomp_symbol_"+(1E9*Math.random()>>>0)+"_",e=0;return b});
z("Symbol.iterator",function(a){if(a)return a;a=Symbol("Symbol.iterator");for(var b="Array Int8Array Uint8Array Uint8ClampedArray Int16Array Uint16Array Int32Array Uint32Array Float32Array Float64Array".split(" "),c=0;c<b.length;c++){var d=y[b[c]];"function"===typeof d&&"function"!=typeof d.prototype[a]&&ba(d.prototype,a,{configurable:!0,writable:!0,value:function(){return da(aa(this))}})}return a});function da(a){a={next:a};a[Symbol.iterator]=function(){return this};return a}
function A(a){var b="undefined"!=typeof Symbol&&Symbol.iterator&&a[Symbol.iterator];return b?b.call(a):{next:aa(a)}}function ea(a){if(!(a instanceof Array)){a=A(a);for(var b,c=[];!(b=a.next()).done;)c.push(b.value);a=c}return a}var fa="function"==typeof Object.assign?Object.assign:function(a,b){for(var c=1;c<arguments.length;c++){var d=arguments[c];if(d)for(var e in d)Object.prototype.hasOwnProperty.call(d,e)&&(a[e]=d[e])}return a};z("Object.assign",function(a){return a||fa});
var ha="function"==typeof Object.create?Object.create:function(a){function b(){}b.prototype=a;return new b},ia;if("function"==typeof Object.setPrototypeOf)ia=Object.setPrototypeOf;else{var ja;a:{var ka={a:!0},la={};try{la.__proto__=ka;ja=la.a;break a}catch(a){}ja=!1}ia=ja?function(a,b){a.__proto__=b;if(a.__proto__!==b)throw new TypeError(a+" is not extensible");return a}:null}var ma=ia;
function na(a,b){a.prototype=ha(b.prototype);a.prototype.constructor=a;if(ma)ma(a,b);else for(var c in b)if("prototype"!=c)if(Object.defineProperties){var d=Object.getOwnPropertyDescriptor(b,c);d&&Object.defineProperty(a,c,d)}else a[c]=b[c];a.za=b.prototype}function oa(){this.m=!1;this.j=null;this.i=void 0;this.h=1;this.v=this.s=0;this.l=null}function pa(a){if(a.m)throw new TypeError("Generator is already running");a.m=!0}oa.prototype.u=function(a){this.i=a};
function qa(a,b){a.l={ma:b,na:!0};a.h=a.s||a.v}oa.prototype.return=function(a){this.l={return:a};this.h=this.v};function D(a,b,c){a.h=c;return{value:b}}function ra(a){this.h=new oa;this.i=a}function sa(a,b){pa(a.h);var c=a.h.j;if(c)return ta(a,"return"in c?c["return"]:function(d){return{value:d,done:!0}},b,a.h.return);a.h.return(b);return ua(a)}
function ta(a,b,c,d){try{var e=b.call(a.h.j,c);if(!(e instanceof Object))throw new TypeError("Iterator result "+e+" is not an object");if(!e.done)return a.h.m=!1,e;var g=e.value}catch(f){return a.h.j=null,qa(a.h,f),ua(a)}a.h.j=null;d.call(a.h,g);return ua(a)}function ua(a){for(;a.h.h;)try{var b=a.i(a.h);if(b)return a.h.m=!1,{value:b.value,done:!1}}catch(c){a.h.i=void 0,qa(a.h,c)}a.h.m=!1;if(a.h.l){b=a.h.l;a.h.l=null;if(b.na)throw b.ma;return{value:b.return,done:!0}}return{value:void 0,done:!0}}
function va(a){this.next=function(b){pa(a.h);a.h.j?b=ta(a,a.h.j.next,b,a.h.u):(a.h.u(b),b=ua(a));return b};this.throw=function(b){pa(a.h);a.h.j?b=ta(a,a.h.j["throw"],b,a.h.u):(qa(a.h,b),b=ua(a));return b};this.return=function(b){return sa(a,b)};this[Symbol.iterator]=function(){return this}}function wa(a){function b(d){return a.next(d)}function c(d){return a.throw(d)}return new Promise(function(d,e){function g(f){f.done?d(f.value):Promise.resolve(f.value).then(b,c).then(g,e)}g(a.next())})}
function E(a){return wa(new va(new ra(a)))}
z("Promise",function(a){function b(f){this.i=0;this.j=void 0;this.h=[];this.u=!1;var h=this.l();try{f(h.resolve,h.reject)}catch(k){h.reject(k)}}function c(){this.h=null}function d(f){return f instanceof b?f:new b(function(h){h(f)})}if(a)return a;c.prototype.i=function(f){if(null==this.h){this.h=[];var h=this;this.j(function(){h.m()})}this.h.push(f)};var e=y.setTimeout;c.prototype.j=function(f){e(f,0)};c.prototype.m=function(){for(;this.h&&this.h.length;){var f=this.h;this.h=[];for(var h=0;h<f.length;++h){var k=
f[h];f[h]=null;try{k()}catch(l){this.l(l)}}}this.h=null};c.prototype.l=function(f){this.j(function(){throw f;})};b.prototype.l=function(){function f(l){return function(m){k||(k=!0,l.call(h,m))}}var h=this,k=!1;return{resolve:f(this.I),reject:f(this.m)}};b.prototype.I=function(f){if(f===this)this.m(new TypeError("A Promise cannot resolve to itself"));else if(f instanceof b)this.L(f);else{a:switch(typeof f){case "object":var h=null!=f;break a;case "function":h=!0;break a;default:h=!1}h?this.F(f):this.s(f)}};
b.prototype.F=function(f){var h=void 0;try{h=f.then}catch(k){this.m(k);return}"function"==typeof h?this.M(h,f):this.s(f)};b.prototype.m=function(f){this.v(2,f)};b.prototype.s=function(f){this.v(1,f)};b.prototype.v=function(f,h){if(0!=this.i)throw Error("Cannot settle("+f+", "+h+"): Promise already settled in state"+this.i);this.i=f;this.j=h;2===this.i&&this.K();this.H()};b.prototype.K=function(){var f=this;e(function(){if(f.D()){var h=y.console;"undefined"!==typeof h&&h.error(f.j)}},1)};b.prototype.D=
function(){if(this.u)return!1;var f=y.CustomEvent,h=y.Event,k=y.dispatchEvent;if("undefined"===typeof k)return!0;"function"===typeof f?f=new f("unhandledrejection",{cancelable:!0}):"function"===typeof h?f=new h("unhandledrejection",{cancelable:!0}):(f=y.document.createEvent("CustomEvent"),f.initCustomEvent("unhandledrejection",!1,!0,f));f.promise=this;f.reason=this.j;return k(f)};b.prototype.H=function(){if(null!=this.h){for(var f=0;f<this.h.length;++f)g.i(this.h[f]);this.h=null}};var g=new c;b.prototype.L=
function(f){var h=this.l();f.T(h.resolve,h.reject)};b.prototype.M=function(f,h){var k=this.l();try{f.call(h,k.resolve,k.reject)}catch(l){k.reject(l)}};b.prototype.then=function(f,h){function k(p,n){return"function"==typeof p?function(q){try{l(p(q))}catch(t){m(t)}}:n}var l,m,r=new b(function(p,n){l=p;m=n});this.T(k(f,l),k(h,m));return r};b.prototype.catch=function(f){return this.then(void 0,f)};b.prototype.T=function(f,h){function k(){switch(l.i){case 1:f(l.j);break;case 2:h(l.j);break;default:throw Error("Unexpected state: "+
l.i);}}var l=this;null==this.h?g.i(k):this.h.push(k);this.u=!0};b.resolve=d;b.reject=function(f){return new b(function(h,k){k(f)})};b.race=function(f){return new b(function(h,k){for(var l=A(f),m=l.next();!m.done;m=l.next())d(m.value).T(h,k)})};b.all=function(f){var h=A(f),k=h.next();return k.done?d([]):new b(function(l,m){function r(q){return function(t){p[q]=t;n--;0==n&&l(p)}}var p=[],n=0;do p.push(void 0),n++,d(k.value).T(r(p.length-1),m),k=h.next();while(!k.done)})};return b});
function xa(a,b){a instanceof String&&(a+="");var c=0,d=!1,e={next:function(){if(!d&&c<a.length){var g=c++;return{value:b(g,a[g]),done:!1}}d=!0;return{done:!0,value:void 0}}};e[Symbol.iterator]=function(){return e};return e}z("Array.prototype.keys",function(a){return a?a:function(){return xa(this,function(b){return b})}});
z("Array.prototype.fill",function(a){return a?a:function(b,c,d){var e=this.length||0;0>c&&(c=Math.max(0,e+c));if(null==d||d>e)d=e;d=Number(d);0>d&&(d=Math.max(0,e+d));for(c=Number(c||0);c<d;c++)this[c]=b;return this}});function F(a){return a?a:Array.prototype.fill}z("Int8Array.prototype.fill",F);z("Uint8Array.prototype.fill",F);z("Uint8ClampedArray.prototype.fill",F);z("Int16Array.prototype.fill",F);z("Uint16Array.prototype.fill",F);z("Int32Array.prototype.fill",F);
z("Uint32Array.prototype.fill",F);z("Float32Array.prototype.fill",F);z("Float64Array.prototype.fill",F);z("Object.is",function(a){return a?a:function(b,c){return b===c?0!==b||1/b===1/c:b!==b&&c!==c}});z("Array.prototype.includes",function(a){return a?a:function(b,c){var d=this;d instanceof String&&(d=String(d));var e=d.length;c=c||0;for(0>c&&(c=Math.max(c+e,0));c<e;c++){var g=d[c];if(g===b||Object.is(g,b))return!0}return!1}});
z("String.prototype.includes",function(a){return a?a:function(b,c){if(null==this)throw new TypeError("The 'this' value for String.prototype.includes must not be null or undefined");if(b instanceof RegExp)throw new TypeError("First argument to String.prototype.includes must not be a regular expression");return-1!==this.indexOf(b,c||0)}});var ya=this||self;
function Aa(a,b){a=a.split(".");var c=ya;a[0]in c||"undefined"==typeof c.execScript||c.execScript("var "+a[0]);for(var d;a.length&&(d=a.shift());)a.length||void 0===b?c[d]&&c[d]!==Object.prototype[d]?c=c[d]:c=c[d]={}:c[d]=b};function Ba(a){var b;a:{if(b=ya.navigator)if(b=b.userAgent)break a;b=""}return-1!=b.indexOf(a)};var Ca=Array.prototype.map?function(a,b){return Array.prototype.map.call(a,b,void 0)}:function(a,b){for(var c=a.length,d=Array(c),e="string"===typeof a?a.split(""):a,g=0;g<c;g++)g in e&&(d[g]=b.call(void 0,e[g],g,a));return d};var Da={},Ea=null;function Fa(a){var b=a.length,c=3*b/4;c%3?c=Math.floor(c):-1!="=.".indexOf(a[b-1])&&(c=-1!="=.".indexOf(a[b-2])?c-2:c-1);var d=new Uint8Array(c),e=0;Ga(a,function(g){d[e++]=g});return e!==c?d.subarray(0,e):d}
function Ga(a,b){function c(k){for(;d<a.length;){var l=a.charAt(d++),m=Ea[l];if(null!=m)return m;if(!/^[\s\xa0]*$/.test(l))throw Error("Unknown base64 encoding at char: "+l);}return k}Ha();for(var d=0;;){var e=c(-1),g=c(0),f=c(64),h=c(64);if(64===h&&-1===e)break;b(e<<2|g>>4);64!=f&&(b(g<<4&240|f>>2),64!=h&&b(f<<6&192|h))}}
function Ha(){if(!Ea){Ea={};for(var a="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".split(""),b=["+/=","+/","-_=","-_.","-_"],c=0;5>c;c++){var d=a.concat(b[c].split(""));Da[c]=d;for(var e=0;e<d.length;e++){var g=d[e];void 0===Ea[g]&&(Ea[g]=e)}}}};var Ia="undefined"!==typeof Uint8Array,Ja=!(Ba("Trident")||Ba("MSIE"))&&"function"===typeof ya.btoa;
function Ka(a){if(!Ja){var b;void 0===b&&(b=0);Ha();b=Da[b];for(var c=Array(Math.floor(a.length/3)),d=b[64]||"",e=0,g=0;e<a.length-2;e+=3){var f=a[e],h=a[e+1],k=a[e+2],l=b[f>>2];f=b[(f&3)<<4|h>>4];h=b[(h&15)<<2|k>>6];k=b[k&63];c[g++]=l+f+h+k}l=0;k=d;switch(a.length-e){case 2:l=a[e+1],k=b[(l&15)<<2]||d;case 1:a=a[e],c[g]=b[a>>2]+b[(a&3)<<4|l>>4]+k+d}return c.join("")}for(b="";10240<a.length;)b+=String.fromCharCode.apply(null,a.subarray(0,10240)),a=a.subarray(10240);b+=String.fromCharCode.apply(null,
a);return btoa(b)}var La=RegExp("[-_.]","g");function Ma(a){switch(a){case "-":return"+";case "_":return"/";case ".":return"=";default:return""}}function Na(a){if(!Ja)return Fa(a);La.test(a)&&(a=a.replace(La,Ma));a=atob(a);for(var b=new Uint8Array(a.length),c=0;c<a.length;c++)b[c]=a.charCodeAt(c);return b}var Oa;function Pa(){return Oa||(Oa=new Uint8Array(0))}var Qa={};var Ra="function"===typeof Uint8Array.prototype.slice,G=0,H=0;function Sa(a){var b=0>a;a=Math.abs(a);var c=a>>>0;a=Math.floor((a-c)/4294967296);b&&(c=A(Ta(c,a)),b=c.next().value,a=c.next().value,c=b);G=c>>>0;H=a>>>0}var Ua="function"===typeof BigInt;function Ta(a,b){b=~b;a?a=~a+1:b+=1;return[a,b]};function Va(a,b){this.i=a>>>0;this.h=b>>>0}
function Wa(a){if(!a)return Xa||(Xa=new Va(0,0));if(!/^-?\d+$/.test(a))return null;if(16>a.length)Sa(Number(a));else if(Ua)a=BigInt(a),G=Number(a&BigInt(4294967295))>>>0,H=Number(a>>BigInt(32)&BigInt(4294967295));else{var b=+("-"===a[0]);H=G=0;for(var c=a.length,d=b,e=(c-b)%6+b;e<=c;d=e,e+=6)d=Number(a.slice(d,e)),H*=1E6,G=1E6*G+d,4294967296<=G&&(H+=G/4294967296|0,G%=4294967296);b&&(b=A(Ta(G,H)),a=b.next().value,b=b.next().value,G=a,H=b)}return new Va(G,H)}var Xa;function Ya(a,b){return Error("Invalid wire type: "+a+" (at position "+b+")")}function Za(){return Error("Failed to read varint, encoding is invalid.")}function $a(a,b){return Error("Tried to read past the end of the data "+b+" > "+a)};function K(){throw Error("Invalid UTF8");}function ab(a,b){b=String.fromCharCode.apply(null,b);return null==a?b:a+b}var bb=void 0,cb,db="undefined"!==typeof TextDecoder,eb,fb="undefined"!==typeof TextEncoder;var gb;function hb(a){if(a!==Qa)throw Error("illegal external caller");}function ib(a,b){hb(b);this.V=a;if(null!=a&&0===a.length)throw Error("ByteString should be constructed with non-empty values");}function jb(){return gb||(gb=new ib(null,Qa))}function kb(a){hb(Qa);var b=a.V;b=null==b||Ia&&null!=b&&b instanceof Uint8Array?b:"string"===typeof b?Na(b):null;return null==b?b:a.V=b};function lb(a){if("string"===typeof a)return{buffer:Na(a),C:!1};if(Array.isArray(a))return{buffer:new Uint8Array(a),C:!1};if(a.constructor===Uint8Array)return{buffer:a,C:!1};if(a.constructor===ArrayBuffer)return{buffer:new Uint8Array(a),C:!1};if(a.constructor===ib)return{buffer:kb(a)||Pa(),C:!0};if(a instanceof Uint8Array)return{buffer:new Uint8Array(a.buffer,a.byteOffset,a.byteLength),C:!1};throw Error("Type not convertible to a Uint8Array, expected a Uint8Array, an ArrayBuffer, a base64 encoded string, a ByteString or an Array of numbers");
};function mb(a,b){this.i=null;this.m=!1;this.h=this.j=this.l=0;nb(this,a,b)}function nb(a,b,c){c=void 0===c?{}:c;a.S=void 0===c.S?!1:c.S;b&&(b=lb(b),a.i=b.buffer,a.m=b.C,a.l=0,a.j=a.i.length,a.h=a.l)}mb.prototype.reset=function(){this.h=this.l};function L(a,b){a.h=b;if(b>a.j)throw $a(a.j,b);}
function ob(a){var b=a.i,c=a.h,d=b[c++],e=d&127;if(d&128&&(d=b[c++],e|=(d&127)<<7,d&128&&(d=b[c++],e|=(d&127)<<14,d&128&&(d=b[c++],e|=(d&127)<<21,d&128&&(d=b[c++],e|=d<<28,d&128&&b[c++]&128&&b[c++]&128&&b[c++]&128&&b[c++]&128&&b[c++]&128)))))throw Za();L(a,c);return e}function pb(a,b){if(0>b)throw Error("Tried to read a negative byte length: "+b);var c=a.h,d=c+b;if(d>a.j)throw $a(b,a.j-c);a.h=d;return c}var qb=[];function rb(){this.h=[]}rb.prototype.length=function(){return this.h.length};rb.prototype.end=function(){var a=this.h;this.h=[];return a};function sb(a,b,c){for(;0<c||127<b;)a.h.push(b&127|128),b=(b>>>7|c<<25)>>>0,c>>>=7;a.h.push(b)}function M(a,b){for(;127<b;)a.h.push(b&127|128),b>>>=7;a.h.push(b)};function tb(a,b){if(qb.length){var c=qb.pop();nb(c,a,b);a=c}else a=new mb(a,b);this.h=a;this.j=this.h.h;this.i=this.l=-1;this.setOptions(b)}tb.prototype.setOptions=function(a){a=void 0===a?{}:a;this.ca=void 0===a.ca?!1:a.ca};tb.prototype.reset=function(){this.h.reset();this.j=this.h.h;this.i=this.l=-1};
function ub(a){var b=a.h;if(b.h==b.j)return!1;a.j=a.h.h;var c=ob(a.h)>>>0;b=c>>>3;c&=7;if(!(0<=c&&5>=c))throw Ya(c,a.j);if(1>b)throw Error("Invalid field number: "+b+" (at position "+a.j+")");a.l=b;a.i=c;return!0}
function vb(a){switch(a.i){case 0:if(0!=a.i)vb(a);else a:{a=a.h;for(var b=a.h,c=b+10,d=a.i;b<c;)if(0===(d[b++]&128)){L(a,b);break a}throw Za();}break;case 1:a=a.h;L(a,a.h+8);break;case 2:2!=a.i?vb(a):(b=ob(a.h)>>>0,a=a.h,L(a,a.h+b));break;case 5:a=a.h;L(a,a.h+4);break;case 3:b=a.l;do{if(!ub(a))throw Error("Unmatched start-group tag: stream EOF");if(4==a.i){if(a.l!=b)throw Error("Unmatched end-group tag");break}vb(a)}while(1);break;default:throw Ya(a.i,a.j);}}var wb=[];function xb(){this.j=[];this.i=0;this.h=new rb}function N(a,b){0!==b.length&&(a.j.push(b),a.i+=b.length)}function yb(a,b){if(b=b.R){N(a,a.h.end());for(var c=0;c<b.length;c++)N(a,kb(b[c])||Pa())}};var O="function"===typeof Symbol&&"symbol"===typeof Symbol()?Symbol():void 0;function P(a,b){if(O)return a[O]|=b;if(void 0!==a.A)return a.A|=b;Object.defineProperties(a,{A:{value:b,configurable:!0,writable:!0,enumerable:!1}});return b}function zb(a,b){O?a[O]&&(a[O]&=~b):void 0!==a.A&&(a.A&=~b)}function Q(a){var b;O?b=a[O]:b=a.A;return null==b?0:b}function R(a,b){O?a[O]=b:void 0!==a.A?a.A=b:Object.defineProperties(a,{A:{value:b,configurable:!0,writable:!0,enumerable:!1}})}
function Ab(a){P(a,1);return a}function Bb(a,b){R(b,(a|0)&-51)}function Cb(a,b){R(b,(a|18)&-41)};var Db={};function Eb(a){return null!==a&&"object"===typeof a&&!Array.isArray(a)&&a.constructor===Object}var Fb,Gb=[];R(Gb,23);Fb=Object.freeze(Gb);function Hb(a){if(Q(a.o)&2)throw Error("Cannot mutate an immutable Message");}function Ib(a){var b=a.length;(b=b?a[b-1]:void 0)&&Eb(b)?b.g=1:(b={},a.push((b.g=1,b)))};function Jb(a){var b=a.i+a.G;return a.B||(a.B=a.o[b]={})}function S(a,b){return-1===b?null:b>=a.i?a.B?a.B[b]:void 0:a.o[b+a.G]}function U(a,b,c,d){Hb(a);Kb(a,b,c,d)}function Kb(a,b,c,d){a.j&&(a.j=void 0);b>=a.i||d?Jb(a)[b]=c:(a.o[b+a.G]=c,(a=a.B)&&b in a&&delete a[b])}function Lb(a,b,c,d){var e=S(a,b);Array.isArray(e)||(e=Fb);var g=Q(e);g&1||Ab(e);if(d)g&2||P(e,2),c&1||Object.freeze(e);else{d=!(c&2);var f=g&2;c&1||!f?d&&g&16&&!f&&zb(e,16):(e=Ab(Array.prototype.slice.call(e)),Kb(a,b,e))}return e}
function Mb(a,b){var c=S(a,b);var d=null==c?c:"number"===typeof c||"NaN"===c||"Infinity"===c||"-Infinity"===c?Number(c):void 0;null!=d&&d!==c&&Kb(a,b,d);return d}
function Nb(a,b,c,d,e){a.h||(a.h={});var g=a.h[c],f=Lb(a,c,3,e);if(!g){var h=f;g=[];var k=!!(Q(a.o)&16);f=!!(Q(h)&2);var l=h;!e&&f&&(h=Array.prototype.slice.call(h));for(var m=f,r=0;r<h.length;r++){var p=h[r];var n=b,q=!1;q=void 0===q?!1:q;p=Array.isArray(p)?new n(p):q?new n:void 0;if(void 0!==p){n=p.o;var t=q=Q(n);f&&(t|=2);k&&(t|=16);t!=q&&R(n,t);n=t;m=m||!!(2&n);g.push(p)}}a.h[c]=g;k=Q(h);b=k|33;b=m?b&-9:b|8;k!=b&&(m=h,Object.isFrozen(m)&&(m=Array.prototype.slice.call(m)),R(m,b),h=m);l!==h&&Kb(a,
c,h);(e||d&&f)&&P(g,2);d&&Object.freeze(g);return g}e||(e=Object.isFrozen(g),d&&!e?Object.freeze(g):!d&&e&&(g=Array.prototype.slice.call(g),a.h[c]=g));return g}function Ob(a,b,c){var d=!!(Q(a.o)&2);b=Nb(a,b,c,d,d);a=Lb(a,c,3,d);if(!(d||Q(a)&8)){for(d=0;d<b.length;d++){c=b[d];if(Q(c.o)&2){var e=Pb(c,!1);e.j=c}else e=c;c!==e&&(b[d]=e,a[d]=e.o)}P(a,8)}return b}
function V(a,b,c){if(null!=c&&"number"!==typeof c)throw Error("Value of float/double field must be a number|null|undefined, found "+typeof c+": "+c);U(a,b,c)}function Qb(a,b,c,d,e){Hb(a);var g=Nb(a,c,b,!1,!1);c=null!=d?d:new c;a=Lb(a,b,2,!1);void 0!=e?(g.splice(e,0,c),a.splice(e,0,c.o)):(g.push(c),a.push(c.o));c.C()&&zb(a,8);return c}function Rb(a,b){return null==a?b:a}function W(a,b,c){c=void 0===c?0:c;return Rb(Mb(a,b),c)};var Sb;function Tb(a){switch(typeof a){case "number":return isFinite(a)?a:String(a);case "object":if(a)if(Array.isArray(a)){if(0!==(Q(a)&128))return a=Array.prototype.slice.call(a),Ib(a),a}else{if(Ia&&null!=a&&a instanceof Uint8Array)return Ka(a);if(a instanceof ib){var b=a.V;return null==b?"":"string"===typeof b?b:a.V=Ka(b)}}}return a};function Ub(a,b,c,d){if(null!=a){if(Array.isArray(a))a=Vb(a,b,c,void 0!==d);else if(Eb(a)){var e={},g;for(g in a)e[g]=Ub(a[g],b,c,d);a=e}else a=b(a,d);return a}}function Vb(a,b,c,d){var e=Q(a);d=d?!!(e&16):void 0;a=Array.prototype.slice.call(a);for(var g=0;g<a.length;g++)a[g]=Ub(a[g],b,c,d);c(e,a);return a}function Wb(a){return a.ja===Db?a.toJSON():Tb(a)}function Xb(a,b){a&128&&Ib(b)};function Yb(a,b,c){c=void 0===c?Cb:c;if(null!=a){if(Ia&&a instanceof Uint8Array)return a.length?new ib(new Uint8Array(a),Qa):jb();if(Array.isArray(a)){var d=Q(a);if(d&2)return a;if(b&&!(d&32)&&(d&16||0===d))return R(a,d|2),a;a=Vb(a,Yb,d&4?Cb:c,!0);b=Q(a);b&4&&b&2&&Object.freeze(a);return a}return a.ja===Db?Zb(a):a}}
function $b(a,b,c,d,e,g,f){if(a=a.h&&a.h[c]){d=Q(a);d&2?d=a:(g=Ca(a,Zb),Cb(d,g),Object.freeze(g),d=g);Hb(b);f=null==d?Fb:Ab([]);if(null!=d){g=!!d.length;for(a=0;a<d.length;a++){var h=d[a];g=g&&!(Q(h.o)&2);f[a]=h.o}g=(g?8:0)|1;a=Q(f);(a&g)!==g&&(Object.isFrozen(f)&&(f=Array.prototype.slice.call(f)),R(f,a|g));b.h||(b.h={});b.h[c]=d}else b.h&&(b.h[c]=void 0);Kb(b,c,f,e)}else U(b,c,Yb(d,g,f),e)}function Zb(a){if(Q(a.o)&2)return a;a=Pb(a,!0);P(a.o,2);return a}
function Pb(a,b){var c=a.o,d=[];P(d,16);var e=a.constructor.h;e&&d.push(e);e=a.B;if(e){d.length=c.length;d.fill(void 0,d.length,c.length);var g={};d[d.length-1]=g}0!==(Q(c)&128)&&Ib(d);b=b||a.C()?Cb:Bb;g=a.constructor;Sb=d;d=new g(d);Sb=void 0;a.R&&(d.R=a.R.slice());g=!!(Q(c)&16);for(var f=e?c.length-1:c.length,h=0;h<f;h++)$b(a,d,h-a.G,c[h],!1,g,b);if(e)for(var k in e)$b(a,d,+k,e[k],!0,g,b);return d};function X(a,b,c){null==a&&(a=Sb);Sb=void 0;var d=this.constructor.i||0,e=0<d,g=this.constructor.h,f=!1;if(null==a){a=g?[g]:[];var h=48;var k=!0;e&&(d=0,h|=128);R(a,h)}else{if(!Array.isArray(a))throw Error();if(g&&g!==a[0])throw Error();var l=h=P(a,0);if(k=0!==(16&l))(f=0!==(32&l))||(l|=32);if(e)if(128&l)d=0;else{if(0<a.length){var m=a[a.length-1];if(Eb(m)&&"g"in m){d=0;l|=128;delete m.g;var r=!0,p;for(p in m){r=!1;break}r&&a.pop()}}}else if(128&l)throw Error();h!==l&&R(a,l)}this.G=(g?0:-1)-d;this.h=
void 0;this.o=a;a:{g=this.o.length;d=g-1;if(g&&(g=this.o[d],Eb(g))){this.B=g;this.i=d-this.G;break a}void 0!==b&&-1<b?(this.i=Math.max(b,d+1-this.G),this.B=void 0):this.i=Number.MAX_VALUE}if(!e&&this.B&&"g"in this.B)throw Error('Unexpected "g" flag in sparse object of message that is not a group type.');if(c){b=k&&!f&&!0;e=this.i;var n;for(k=0;k<c.length;k++)f=c[k],f<e?(f+=this.G,(d=a[f])?ac(d,b):a[f]=Fb):(n||(n=Jb(this)),(d=n[f])?ac(d,b):n[f]=Fb)}}
X.prototype.toJSON=function(){return Vb(this.o,Wb,Xb)};X.prototype.C=function(){return!!(Q(this.o)&2)};function ac(a,b){if(Array.isArray(a)){var c=Q(a),d=1;!b||c&2||(d|=16);(c&d)!==d&&R(a,c|d)}}X.prototype.ja=Db;X.prototype.toString=function(){return this.o.toString()};function bc(a,b,c){if(c){var d={},e;for(e in c){var g=c[e],f=g.ra;f||(d.J=g.xa||g.oa.W,g.ia?(d.aa=cc(g.ia),f=function(h){return function(k,l,m){return h.J(k,l,m,h.aa)}}(d)):g.ka?(d.Z=dc(g.da.P,g.ka),f=function(h){return function(k,l,m){return h.J(k,l,m,h.Z)}}(d)):f=d.J,g.ra=f);f(b,a,g.da);d={J:d.J,aa:d.aa,Z:d.Z}}}yb(b,a)}var ec=Symbol();function fc(a,b,c){return a[ec]||(a[ec]=function(d,e){return b(d,e,c)})}
function gc(a){var b=a[ec];if(!b){var c=hc(a);b=function(d,e){return ic(d,e,c)};a[ec]=b}return b}function jc(a){var b=a.ia;if(b)return gc(b);if(b=a.wa)return fc(a.da.P,b,a.ka)}function kc(a){var b=jc(a),c=a.da,d=a.oa.U;return b?function(e,g){return d(e,g,c,b)}:function(e,g){return d(e,g,c)}}function lc(a,b){var c=a[b];"function"==typeof c&&0===c.length&&(c=c(),a[b]=c);return Array.isArray(c)&&(mc in c||nc in c||0<c.length&&"function"==typeof c[0])?c:void 0}
function oc(a,b,c,d,e,g){b.P=a[0];var f=1;if(a.length>f&&"number"!==typeof a[f]){var h=a[f++];c(b,h)}for(;f<a.length;){c=a[f++];for(var k=f+1;k<a.length&&"number"!==typeof a[k];)k++;h=a[f++];k-=f;switch(k){case 0:d(b,c,h);break;case 1:(k=lc(a,f))?(f++,e(b,c,h,k)):d(b,c,h,a[f++]);break;case 2:k=f++;k=lc(a,k);e(b,c,h,k,a[f++]);break;case 3:g(b,c,h,a[f++],a[f++],a[f++]);break;case 4:g(b,c,h,a[f++],a[f++],a[f++],a[f++]);break;default:throw Error("unexpected number of binary field arguments: "+k);}}return b}
var pc=Symbol();function cc(a){var b=a[pc];if(!b){var c=qc(a);b=function(d,e){return rc(d,e,c)};a[pc]=b}return b}function dc(a,b){var c=a[pc];c||(c=function(d,e){return bc(d,e,b)},a[pc]=c);return c}var nc=Symbol();function sc(a,b){a.push(b)}function tc(a,b,c){a.push(b,c.W)}function uc(a,b,c,d){var e=cc(d),g=qc(d).P,f=c.W;a.push(b,function(h,k,l){return f(h,k,l,g,e)})}function vc(a,b,c,d,e,g){var f=dc(d,g),h=c.W;a.push(b,function(k,l,m){return h(k,l,m,d,f)})}
function qc(a){var b=a[nc];if(b)return b;b=oc(a,a[nc]=[],sc,tc,uc,vc);mc in a&&nc in a&&(a.length=0);return b}var mc=Symbol();function wc(a,b){a[0]=b}function xc(a,b,c,d){var e=c.U;a[b]=d?function(g,f,h){return e(g,f,h,d)}:e}function yc(a,b,c,d,e){var g=c.U,f=gc(d),h=hc(d).P;a[b]=function(k,l,m){return g(k,l,m,h,f,e)}}function zc(a,b,c,d,e,g,f){var h=c.U,k=fc(d,e,g);a[b]=function(l,m,r){return h(l,m,r,d,k,f)}}
function hc(a){var b=a[mc];if(b)return b;b=oc(a,a[mc]={},wc,xc,yc,zc);mc in a&&nc in a&&(a.length=0);return b}
function ic(a,b,c){for(;ub(b)&&4!=b.i;){var d=b.l,e=c[d];if(!e){var g=c[0];g&&(g=g[d])&&(e=c[d]=kc(g))}if(!e||!e(b,a,d)){e=b;d=a;g=e.j;vb(e);var f=e;if(!f.ca){e=f.h.h-g;f.h.h=g;f=f.h;if(0==e)e=jb();else{g=pb(f,e);if(f.S&&f.m)e=f.i.subarray(g,g+e);else{f=f.i;var h=g;e=g+e;e=h===e?Pa():Ra?f.slice(h,e):new Uint8Array(f.subarray(h,e))}e=0==e.length?jb():new ib(e,Qa)}(g=d.R)?g.push(e):d.R=[e]}}}return a}
function rc(a,b,c){for(var d=c.length,e=1==d%2,g=e?1:0;g<d;g+=2)(0,c[g+1])(b,a,c[g]);bc(a,b,e?c[0]:void 0)}function Ac(a,b){return{U:a,W:b}}
var Y=Ac(function(a,b,c){if(5!==a.i)return!1;a=a.h;var d=a.i,e=a.h,g=d[e];var f=d[e+1];var h=d[e+2];d=d[e+3];L(a,a.h+4);f=(g<<0|f<<8|h<<16|d<<24)>>>0;a=2*(f>>31)+1;g=f>>>23&255;f&=8388607;U(b,c,255==g?f?NaN:Infinity*a:0==g?a*Math.pow(2,-149)*f:a*Math.pow(2,g-150)*(f+Math.pow(2,23)));return!0},function(a,b,c){b=Mb(b,c);if(null!=b){M(a.h,8*c+5);a=a.h;var d=+b;0===d?0<1/d?G=H=0:(H=0,G=2147483648):isNaN(d)?(H=0,G=2147483647):(d=(c=0>d?-2147483648:0)?-d:d,3.4028234663852886E38<d?(H=0,G=(c|2139095040)>>>
0):1.1754943508222875E-38>d?(d=Math.round(d/Math.pow(2,-149)),H=0,G=(c|d)>>>0):(b=Math.floor(Math.log(d)/Math.LN2),d*=Math.pow(2,-b),d=Math.round(8388608*d),16777216<=d&&++b,H=0,G=(c|b+127<<23|d&8388607)>>>0));c=G;a.h.push(c>>>0&255);a.h.push(c>>>8&255);a.h.push(c>>>16&255);a.h.push(c>>>24&255)}}),Bc=Ac(function(a,b,c){if(0!==a.i)return!1;var d=a.h,e=0,g=a=0,f=d.i,h=d.h;do{var k=f[h++];e|=(k&127)<<g;g+=7}while(32>g&&k&128);32<g&&(a|=(k&127)>>4);for(g=3;32>g&&k&128;g+=7)k=f[h++],a|=(k&127)<<g;L(d,
h);if(128>k){d=e>>>0;k=a>>>0;if(a=k&2147483648)d=~d+1>>>0,k=~k>>>0,0==d&&(k=k+1>>>0);d=4294967296*k+(d>>>0)}else throw Za();U(b,c,a?-d:d);return!0},function(a,b,c){b=S(b,c);null!=b&&("string"===typeof b&&Wa(b),null!=b&&(M(a.h,8*c),"number"===typeof b?(a=a.h,Sa(b),sb(a,G,H)):(c=Wa(b),sb(a.h,c.i,c.h))))}),Cc=Ac(function(a,b,c){if(0!==a.i)return!1;U(b,c,ob(a.h));return!0},function(a,b,c){b=S(b,c);if(null!=b&&null!=b)if(M(a.h,8*c),a=a.h,c=b,0<=c)M(a,c);else{for(b=0;9>b;b++)a.h.push(c&127|128),c>>=7;a.h.push(1)}}),
Dc=Ac(function(a,b,c){if(2!==a.i)return!1;var d=ob(a.h)>>>0;a=a.h;var e=pb(a,d);a=a.i;if(db){var g=a,f;(f=cb)||(f=cb=new TextDecoder("utf-8",{fatal:!0}));a=e+d;g=0===e&&a===g.length?g:g.subarray(e,a);try{var h=f.decode(g)}catch(r){if(void 0===bb){try{f.decode(new Uint8Array([128]))}catch(p){}try{f.decode(new Uint8Array([97])),bb=!0}catch(p){bb=!1}}!bb&&(cb=void 0);throw r;}}else{h=e;d=h+d;e=[];for(var k=null,l,m;h<d;)l=a[h++],128>l?e.push(l):224>l?h>=d?K():(m=a[h++],194>l||128!==(m&192)?(h--,K()):
e.push((l&31)<<6|m&63)):240>l?h>=d-1?K():(m=a[h++],128!==(m&192)||224===l&&160>m||237===l&&160<=m||128!==((g=a[h++])&192)?(h--,K()):e.push((l&15)<<12|(m&63)<<6|g&63)):244>=l?h>=d-2?K():(m=a[h++],128!==(m&192)||0!==(l<<28)+(m-144)>>30||128!==((g=a[h++])&192)||128!==((f=a[h++])&192)?(h--,K()):(l=(l&7)<<18|(m&63)<<12|(g&63)<<6|f&63,l-=65536,e.push((l>>10&1023)+55296,(l&1023)+56320))):K(),8192<=e.length&&(k=ab(k,e),e.length=0);h=ab(k,e)}U(b,c,h);return!0},function(a,b,c){b=S(b,c);if(null!=b){var d=!1;
d=void 0===d?!1:d;if(fb){if(d&&/(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])/.test(b))throw Error("Found an unpaired surrogate");b=(eb||(eb=new TextEncoder)).encode(b)}else{for(var e=0,g=new Uint8Array(3*b.length),f=0;f<b.length;f++){var h=b.charCodeAt(f);if(128>h)g[e++]=h;else{if(2048>h)g[e++]=h>>6|192;else{if(55296<=h&&57343>=h){if(56319>=h&&f<b.length){var k=b.charCodeAt(++f);if(56320<=k&&57343>=k){h=1024*(h-55296)+k-56320+65536;g[e++]=h>>18|240;g[e++]=h>>12&63|128;
g[e++]=h>>6&63|128;g[e++]=h&63|128;continue}else f--}if(d)throw Error("Found an unpaired surrogate");h=65533}g[e++]=h>>12|224;g[e++]=h>>6&63|128}g[e++]=h&63|128}}b=e===g.length?g:g.subarray(0,e)}M(a.h,8*c+2);M(a.h,b.length);N(a,a.h.end());N(a,b)}}),Ec=Ac(function(a,b,c,d,e){if(2!==a.i)return!1;b=Qb(b,c,d);c=a.h.j;d=ob(a.h)>>>0;var g=a.h.h+d,f=g-c;0>=f&&(a.h.j=g,e(b,a,void 0,void 0,void 0),f=g-a.h.h);if(f)throw Error("Message parsing ended unexpectedly. Expected to read "+(d+" bytes, instead read "+
(d-f)+" bytes, either the data ended unexpectedly or the message misreported its own length"));a.h.h=g;a.h.j=c;return!0},function(a,b,c,d,e){b=Ob(b,d,c);if(null!=b)for(d=0;d<b.length;d++){var g=a;M(g.h,8*c+2);var f=g.h.end();N(g,f);f.push(g.i);g=f;e(b[d],a);f=a;var h=g.pop();for(h=f.i+f.h.length()-h;127<h;)g.push(h&127|128),h>>>=7,f.i++;g.push(h);f.i++}});function Fc(a){return function(b,c){a:{if(wb.length){var d=wb.pop();d.setOptions(c);nb(d.h,b,c);b=d}else b=new tb(b,c);try{var e=hc(a);var g=ic(new e.P,b,e);break a}finally{e=b.h,e.i=null,e.m=!1,e.l=0,e.j=0,e.h=0,e.S=!1,b.l=-1,b.i=-1,100>wb.length&&wb.push(b)}g=void 0}return g}}function Gc(a){return function(){var b=new xb;rc(this,b,qc(a));N(b,b.h.end());for(var c=new Uint8Array(b.i),d=b.j,e=d.length,g=0,f=0;f<e;f++){var h=d[f];c.set(h,g);g+=h.length}b.j=[c];return c}};function Z(a){X.call(this,a)}na(Z,X);var Hc=[Z,1,Cc,2,Y,3,Dc,4,Dc];Z.prototype.l=Gc(Hc);function Ic(a){X.call(this,a,-1,Jc)}na(Ic,X);Ic.prototype.addClassification=function(a,b){Qb(this,1,Z,a,b);return this};var Jc=[1],Kc=Fc([Ic,1,Ec,Hc]);function Lc(a){X.call(this,a)}na(Lc,X);var Mc=[Lc,1,Y,2,Y,3,Y,4,Y,5,Y];Lc.prototype.l=Gc(Mc);function Nc(a){X.call(this,a,-1,Oc)}na(Nc,X);var Oc=[1],Pc=Fc([Nc,1,Ec,Mc]);function Qc(a){X.call(this,a)}na(Qc,X);var Rc=[Qc,1,Y,2,Y,3,Y,4,Y,5,Y,6,Bc],Sc=Fc(Rc);Qc.prototype.l=Gc(Rc);function Tc(a,b,c){c=a.createShader(0===c?a.VERTEX_SHADER:a.FRAGMENT_SHADER);a.shaderSource(c,b);a.compileShader(c);if(!a.getShaderParameter(c,a.COMPILE_STATUS))throw Error("Could not compile WebGL shader.\n\n"+a.getShaderInfoLog(c));return c};function Uc(a){return Ob(a,Z,1).map(function(b){var c=S(b,1);return{index:null==c?0:c,qa:W(b,2),label:null!=S(b,3)?Rb(S(b,3),""):void 0,displayName:null!=S(b,4)?Rb(S(b,4),""):void 0}})};function Vc(a){return{x:W(a,1),y:W(a,2),z:W(a,3),visibility:null!=Mb(a,4)?W(a,4):void 0}};function Wc(a,b){this.i=a;this.h=b;this.m=0}
function Xc(a,b,c){Yc(a,b);if("function"===typeof a.h.canvas.transferToImageBitmap)return Promise.resolve(a.h.canvas.transferToImageBitmap());if(c)return Promise.resolve(a.h.canvas);if("function"===typeof createImageBitmap)return createImageBitmap(a.h.canvas);void 0===a.j&&(a.j=document.createElement("canvas"));return new Promise(function(d){a.j.height=a.h.canvas.height;a.j.width=a.h.canvas.width;a.j.getContext("2d",{}).drawImage(a.h.canvas,0,0,a.h.canvas.width,a.h.canvas.height);d(a.j)})}
function Yc(a,b){var c=a.h;if(void 0===a.s){var d=Tc(c,"\n attribute vec2 aVertex;\n attribute vec2 aTex;\n varying vec2 vTex;\n void main(void) {\n gl_Position = vec4(aVertex, 0.0, 1.0);\n vTex = aTex;\n }",0),e=Tc(c,"\n precision mediump float;\n varying vec2 vTex;\n uniform sampler2D sampler0;\n void main(){\n gl_FragColor = texture2D(sampler0, vTex);\n }",1),g=c.createProgram();c.attachShader(g,d);c.attachShader(g,e);c.linkProgram(g);if(!c.getProgramParameter(g,c.LINK_STATUS))throw Error("Could not compile WebGL program.\n\n"+
c.getProgramInfoLog(g));d=a.s=g;c.useProgram(d);e=c.getUniformLocation(d,"sampler0");a.l={O:c.getAttribLocation(d,"aVertex"),N:c.getAttribLocation(d,"aTex"),ya:e};a.v=c.createBuffer();c.bindBuffer(c.ARRAY_BUFFER,a.v);c.enableVertexAttribArray(a.l.O);c.vertexAttribPointer(a.l.O,2,c.FLOAT,!1,0,0);c.bufferData(c.ARRAY_BUFFER,new Float32Array([-1,-1,-1,1,1,1,1,-1]),c.STATIC_DRAW);c.bindBuffer(c.ARRAY_BUFFER,null);a.u=c.createBuffer();c.bindBuffer(c.ARRAY_BUFFER,a.u);c.enableVertexAttribArray(a.l.N);c.vertexAttribPointer(a.l.N,
2,c.FLOAT,!1,0,0);c.bufferData(c.ARRAY_BUFFER,new Float32Array([0,1,0,0,1,0,1,1]),c.STATIC_DRAW);c.bindBuffer(c.ARRAY_BUFFER,null);c.uniform1i(e,0)}d=a.l;c.useProgram(a.s);c.canvas.width=b.width;c.canvas.height=b.height;c.viewport(0,0,b.width,b.height);c.activeTexture(c.TEXTURE0);a.i.bindTexture2d(b.glName);c.enableVertexAttribArray(d.O);c.bindBuffer(c.ARRAY_BUFFER,a.v);c.vertexAttribPointer(d.O,2,c.FLOAT,!1,0,0);c.enableVertexAttribArray(d.N);c.bindBuffer(c.ARRAY_BUFFER,a.u);c.vertexAttribPointer(d.N,
2,c.FLOAT,!1,0,0);c.bindFramebuffer(c.DRAW_FRAMEBUFFER?c.DRAW_FRAMEBUFFER:c.FRAMEBUFFER,null);c.clearColor(0,0,0,0);c.clear(c.COLOR_BUFFER_BIT);c.colorMask(!0,!0,!0,!0);c.drawArrays(c.TRIANGLE_FAN,0,4);c.disableVertexAttribArray(d.O);c.disableVertexAttribArray(d.N);c.bindBuffer(c.ARRAY_BUFFER,null);a.i.bindTexture2d(0)}function Zc(a){this.h=a};var $c=new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,9,1,7,0,65,0,253,15,26,11]);function ad(a,b){return b+a}function bd(a,b){window[a]=b}function cd(a){var b=document.createElement("script");b.setAttribute("src",a);b.setAttribute("crossorigin","anonymous");return new Promise(function(c){b.addEventListener("load",function(){c()},!1);b.addEventListener("error",function(){c()},!1);document.body.appendChild(b)})}
function dd(){return E(function(a){switch(a.h){case 1:return a.s=2,D(a,WebAssembly.instantiate($c),4);case 4:a.h=3;a.s=0;break;case 2:return a.s=0,a.l=null,a.return(!1);case 3:return a.return(!0)}})}
function ed(a){this.h=a;this.listeners={};this.l={};this.L={};this.s={};this.v={};this.M=this.u=this.ga=!0;this.I=Promise.resolve();this.fa="";this.D={};this.locateFile=a&&a.locateFile||ad;if("object"===typeof window)var b=window.location.pathname.toString().substring(0,window.location.pathname.toString().lastIndexOf("/"))+"/";else if("undefined"!==typeof location)b=location.pathname.toString().substring(0,location.pathname.toString().lastIndexOf("/"))+"/";else throw Error("solutions can only be loaded on a web page or in a web worker");
this.ha=b;if(a.options){b=A(Object.keys(a.options));for(var c=b.next();!c.done;c=b.next()){c=c.value;var d=a.options[c].default;void 0!==d&&(this.l[c]="function"===typeof d?d():d)}}}x=ed.prototype;x.close=function(){this.j&&this.j.delete();return Promise.resolve()};
function fd(a){var b,c,d,e,g,f,h,k,l,m,r;return E(function(p){switch(p.h){case 1:if(!a.ga)return p.return();b=void 0===a.h.files?[]:"function"===typeof a.h.files?a.h.files(a.l):a.h.files;return D(p,dd(),2);case 2:c=p.i;if("object"===typeof window)return bd("createMediapipeSolutionsWasm",{locateFile:a.locateFile}),bd("createMediapipeSolutionsPackedAssets",{locateFile:a.locateFile}),f=b.filter(function(n){return void 0!==n.data}),h=b.filter(function(n){return void 0===n.data}),k=Promise.all(f.map(function(n){var q=
gd(a,n.url);if(void 0!==n.path){var t=n.path;q=q.then(function(w){a.overrideFile(t,w);return Promise.resolve(w)})}return q})),l=Promise.all(h.map(function(n){return void 0===n.simd||n.simd&&c||!n.simd&&!c?cd(a.locateFile(n.url,a.ha)):Promise.resolve()})).then(function(){var n,q,t;return E(function(w){if(1==w.h)return n=window.createMediapipeSolutionsWasm,q=window.createMediapipeSolutionsPackedAssets,t=a,D(w,n(q),2);t.i=w.i;w.h=0})}),m=function(){return E(function(n){a.h.graph&&a.h.graph.url?n=D(n,
gd(a,a.h.graph.url),0):(n.h=0,n=void 0);return n})}(),D(p,Promise.all([l,k,m]),7);if("function"!==typeof importScripts)throw Error("solutions can only be loaded on a web page or in a web worker");d=b.filter(function(n){return void 0===n.simd||n.simd&&c||!n.simd&&!c}).map(function(n){return a.locateFile(n.url,a.ha)});importScripts.apply(null,ea(d));e=a;return D(p,createMediapipeSolutionsWasm(Module),6);case 6:e.i=p.i;a.m=new OffscreenCanvas(1,1);a.i.canvas=a.m;g=a.i.GL.createContext(a.m,{antialias:!1,
alpha:!1,va:"undefined"!==typeof WebGL2RenderingContext?2:1});a.i.GL.makeContextCurrent(g);p.h=4;break;case 7:a.m=document.createElement("canvas");r=a.m.getContext("webgl2",{});if(!r&&(r=a.m.getContext("webgl",{}),!r))return alert("Failed to create WebGL canvas context when passing video frame."),p.return();a.K=r;a.i.canvas=a.m;a.i.createContext(a.m,!0,!0,{});case 4:a.j=new a.i.SolutionWasm,a.ga=!1,p.h=0}})}
function hd(a){var b,c,d,e,g,f,h,k;return E(function(l){if(1==l.h){if(a.h.graph&&a.h.graph.url&&a.fa===a.h.graph.url)return l.return();a.u=!0;if(!a.h.graph||!a.h.graph.url){l.h=2;return}a.fa=a.h.graph.url;return D(l,gd(a,a.h.graph.url),3)}2!=l.h&&(b=l.i,a.j.loadGraph(b));c=A(Object.keys(a.D));for(d=c.next();!d.done;d=c.next())e=d.value,a.j.overrideFile(e,a.D[e]);a.D={};if(a.h.listeners)for(g=A(a.h.listeners),f=g.next();!f.done;f=g.next())h=f.value,id(a,h);k=a.l;a.l={};a.setOptions(k);l.h=0})}
x.reset=function(){var a=this;return E(function(b){a.j&&(a.j.reset(),a.s={},a.v={});b.h=0})};
x.setOptions=function(a,b){var c=this;if(b=b||this.h.options){for(var d=[],e=[],g={},f=A(Object.keys(a)),h=f.next();!h.done;g={X:g.X,Y:g.Y},h=f.next())if(h=h.value,!(h in this.l&&this.l[h]===a[h])){this.l[h]=a[h];var k=b[h];void 0!==k&&(k.onChange&&(g.X=k.onChange,g.Y=a[h],d.push(function(l){return function(){var m;return E(function(r){if(1==r.h)return D(r,l.X(l.Y),2);m=r.i;!0===m&&(c.u=!0);r.h=0})}}(g))),k.graphOptionXref&&(h=Object.assign({},{calculatorName:"",calculatorIndex:0},k.graphOptionXref,
{valueNumber:1===k.type?a[h]:0,valueBoolean:0===k.type?a[h]:!1,valueString:2===k.type?a[h]:""}),e.push(h)))}if(0!==d.length||0!==e.length)this.u=!0,this.H=(void 0===this.H?[]:this.H).concat(e),this.F=(void 0===this.F?[]:this.F).concat(d)}};
function jd(a){var b,c,d,e,g,f,h;return E(function(k){switch(k.h){case 1:if(!a.u)return k.return();if(!a.F){k.h=2;break}b=A(a.F);c=b.next();case 3:if(c.done){k.h=5;break}d=c.value;return D(k,d(),4);case 4:c=b.next();k.h=3;break;case 5:a.F=void 0;case 2:if(a.H){e=new a.i.GraphOptionChangeRequestList;g=A(a.H);for(f=g.next();!f.done;f=g.next())h=f.value,e.push_back(h);a.j.changeOptions(e);e.delete();a.H=void 0}a.u=!1;k.h=0}})}
x.initialize=function(){var a=this;return E(function(b){return 1==b.h?D(b,fd(a),2):3!=b.h?D(b,hd(a),3):D(b,jd(a),0)})};function gd(a,b){var c,d;return E(function(e){if(b in a.L)return e.return(a.L[b]);c=a.locateFile(b,"");d=fetch(c).then(function(g){return g.arrayBuffer()});a.L[b]=d;return e.return(d)})}x.overrideFile=function(a,b){this.j?this.j.overrideFile(a,b):this.D[a]=b};x.clearOverriddenFiles=function(){this.D={};this.j&&this.j.clearOverriddenFiles()};
x.send=function(a,b){var c=this,d,e,g,f,h,k,l,m,r;return E(function(p){switch(p.h){case 1:if(!c.h.inputs)return p.return();d=1E3*(void 0===b||null===b?performance.now():b);return D(p,c.I,2);case 2:return D(p,c.initialize(),3);case 3:e=new c.i.PacketDataList;g=A(Object.keys(a));for(f=g.next();!f.done;f=g.next())if(h=f.value,k=c.h.inputs[h]){a:{var n=a[h];switch(k.type){case "video":var q=c.s[k.stream];q||(q=new Wc(c.i,c.K),c.s[k.stream]=q);0===q.m&&(q.m=q.i.createTexture());if("undefined"!==typeof HTMLVideoElement&&
n instanceof HTMLVideoElement){var t=n.videoWidth;var w=n.videoHeight}else"undefined"!==typeof HTMLImageElement&&n instanceof HTMLImageElement?(t=n.naturalWidth,w=n.naturalHeight):(t=n.width,w=n.height);w={glName:q.m,width:t,height:w};t=q.h;t.canvas.width=w.width;t.canvas.height=w.height;t.activeTexture(t.TEXTURE0);q.i.bindTexture2d(q.m);t.texImage2D(t.TEXTURE_2D,0,t.RGBA,t.RGBA,t.UNSIGNED_BYTE,n);q.i.bindTexture2d(0);q=w;break a;case "detections":q=c.s[k.stream];q||(q=new Zc(c.i),c.s[k.stream]=q);
q.data||(q.data=new q.h.DetectionListData);q.data.reset(n.length);for(w=0;w<n.length;++w){t=n[w];var v=q.data,B=v.setBoundingBox,J=w;var I=t.la;var u=new Qc;V(u,1,I.sa);V(u,2,I.ta);V(u,3,I.height);V(u,4,I.width);V(u,5,I.rotation);U(u,6,I.pa);I=u.l();B.call(v,J,I);if(t.ea)for(v=0;v<t.ea.length;++v){u=t.ea[v];B=q.data;J=B.addNormalizedLandmark;I=w;u=Object.assign({},u,{visibility:u.visibility?u.visibility:0});var C=new Lc;V(C,1,u.x);V(C,2,u.y);V(C,3,u.z);u.visibility&&V(C,4,u.visibility);u=C.l();J.call(B,
I,u)}if(t.ba)for(v=0;v<t.ba.length;++v)B=q.data,J=B.addClassification,I=w,u=t.ba[v],C=new Z,V(C,2,u.qa),u.index&&U(C,1,u.index),u.label&&U(C,3,u.label),u.displayName&&U(C,4,u.displayName),u=C.l(),J.call(B,I,u)}q=q.data;break a;default:q={}}}l=q;m=k.stream;switch(k.type){case "video":e.pushTexture2d(Object.assign({},l,{stream:m,timestamp:d}));break;case "detections":r=l;r.stream=m;r.timestamp=d;e.pushDetectionList(r);break;default:throw Error("Unknown input config type: '"+k.type+"'");}}c.j.send(e);
return D(p,c.I,4);case 4:e.delete(),p.h=0}})};
function kd(a,b,c){var d,e,g,f,h,k,l,m,r,p,n,q,t,w;return E(function(v){switch(v.h){case 1:if(!c)return v.return(b);d={};e=0;g=A(Object.keys(c));for(f=g.next();!f.done;f=g.next())h=f.value,k=c[h],"string"!==typeof k&&"texture"===k.type&&void 0!==b[k.stream]&&++e;1<e&&(a.M=!1);l=A(Object.keys(c));f=l.next();case 2:if(f.done){v.h=4;break}m=f.value;r=c[m];if("string"===typeof r)return t=d,w=m,D(v,ld(a,m,b[r]),14);p=b[r.stream];if("detection_list"===r.type){if(p){var B=p.getRectList();for(var J=p.getLandmarksList(),
I=p.getClassificationsList(),u=[],C=0;C<B.size();++C){var T=Sc(B.get(C)),od=W(T,1),pd=W(T,2),qd=W(T,3),rd=W(T,4),sd=W(T,5,0),za=void 0;za=void 0===za?0:za;T={la:{sa:od,ta:pd,height:qd,width:rd,rotation:sd,pa:Rb(S(T,6),za)},ea:Ob(Pc(J.get(C)),Lc,1).map(Vc),ba:Uc(Kc(I.get(C)))};u.push(T)}B=u}else B=[];d[m]=B;v.h=7;break}if("proto_list"===r.type){if(p){B=Array(p.size());for(J=0;J<p.size();J++)B[J]=p.get(J);p.delete()}else B=[];d[m]=B;v.h=7;break}if(void 0===p){v.h=3;break}if("float_list"===r.type){d[m]=
p;v.h=7;break}if("proto"===r.type){d[m]=p;v.h=7;break}if("texture"!==r.type)throw Error("Unknown output config type: '"+r.type+"'");n=a.v[m];n||(n=new Wc(a.i,a.K),a.v[m]=n);return D(v,Xc(n,p,a.M),13);case 13:q=v.i,d[m]=q;case 7:r.transform&&d[m]&&(d[m]=r.transform(d[m]));v.h=3;break;case 14:t[w]=v.i;case 3:f=l.next();v.h=2;break;case 4:return v.return(d)}})}
function ld(a,b,c){var d;return E(function(e){return"number"===typeof c||c instanceof Uint8Array||c instanceof a.i.Uint8BlobList?e.return(c):c instanceof a.i.Texture2dDataOut?(d=a.v[b],d||(d=new Wc(a.i,a.K),a.v[b]=d),e.return(Xc(d,c,a.M))):e.return(void 0)})}
function id(a,b){for(var c=b.name||"$",d=[].concat(ea(b.wants)),e=new a.i.StringList,g=A(b.wants),f=g.next();!f.done;f=g.next())e.push_back(f.value);g=a.i.PacketListener.implement({onResults:function(h){for(var k={},l=0;l<b.wants.length;++l)k[d[l]]=h.get(l);var m=a.listeners[c];m&&(a.I=kd(a,k,b.outs).then(function(r){r=m(r);for(var p=0;p<b.wants.length;++p){var n=k[d[p]];"object"===typeof n&&n.hasOwnProperty&&n.hasOwnProperty("delete")&&n.delete()}r&&(a.I=r)}))}});a.j.attachMultiListener(e,g);e.delete()}
x.onResults=function(a,b){this.listeners[b||"$"]=a};Aa("Solution",ed);Aa("OptionType",{BOOL:0,NUMBER:1,ua:2,0:"BOOL",1:"NUMBER",2:"STRING"});function md(a){void 0===a&&(a=0);switch(a){case 1:return"selfie_segmentation_landscape.tflite";default:return"selfie_segmentation.tflite"}}
function nd(a){var b=this;a=a||{};this.h=new ed({locateFile:a.locateFile,files:function(c){return[{simd:!0,url:"selfie_segmentation_solution_simd_wasm_bin.js"},{simd:!1,url:"selfie_segmentation_solution_wasm_bin.js"},{data:!0,url:md(c.modelSelection)}]},graph:{url:"selfie_segmentation.binarypb"},listeners:[{wants:["segmentation_mask","image_transformed"],outs:{image:{type:"texture",stream:"image_transformed"},segmentationMask:{type:"texture",stream:"segmentation_mask"}}}],inputs:{image:{type:"video",
stream:"input_frames_gpu"}},options:{useCpuInference:{type:0,graphOptionXref:{calculatorType:"InferenceCalculator",fieldName:"use_cpu_inference"},default:"object"!==typeof window||void 0===window.navigator?!1:"iPad Simulator;iPhone Simulator;iPod Simulator;iPad;iPhone;iPod".split(";").includes(navigator.platform)||navigator.userAgent.includes("Mac")&&"ontouchend"in document},selfieMode:{type:0,graphOptionXref:{calculatorType:"GlScalerCalculator",calculatorIndex:1,fieldName:"flip_horizontal"}},modelSelection:{type:1,
graphOptionXref:{calculatorType:"ConstantSidePacketCalculator",calculatorName:"ConstantSidePacketCalculatorModelSelection",fieldName:"int_value"},onChange:function(c){var d,e,g;return E(function(f){if(1==f.h)return d=md(c),e="third_party/mediapipe/modules/selfie_segmentation/"+d,D(f,gd(b.h,d),2);g=f.i;b.h.overrideFile(e,g);return f.return(!0)})}}}})}x=nd.prototype;x.close=function(){this.h.close();return Promise.resolve()};x.onResults=function(a){this.h.onResults(a)};
x.initialize=function(){var a=this;return E(function(b){return D(b,a.h.initialize(),0)})};x.reset=function(){this.h.reset()};x.send=function(a){var b=this;return E(function(c){return D(c,b.h.send(a),0)})};x.setOptions=function(a){this.h.setOptions(a)};Aa("SelfieSegmentation",nd);Aa("VERSION","0.1.1675465747");}).call(this);

83
public/js/tailwind.js Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

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