Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
974223f7e7 | ||
|
|
f10d5ad5ed | ||
|
|
a228392dc6 | ||
|
|
b7b6c4a2ab | ||
|
|
c655ba713b | ||
|
|
0b3cacf4b3 | ||
|
|
1eb089f6b5 | ||
|
|
744def72fd | ||
|
|
1c66c31f6d | ||
|
|
df34d66d38 | ||
|
|
85556dabb4 | ||
|
|
230430c91c | ||
|
|
285ba029f4 | ||
|
|
2b32b3712f | ||
|
|
7affff6e15 | ||
|
|
098c0105f5 | ||
|
|
a21d370b91 | ||
|
|
a2da5cdc89 | ||
|
|
362a7da74f |
@@ -14,29 +14,64 @@ on:
|
|||||||
- master
|
- master
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# Only one build per ref at a time: two QEMU-emulated Gradle builds in parallel
|
||||||
|
# take the ARM64 runner down (both runs then die at the same instant).
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
# Keep in sync with .github/workflows/build-ios.yaml and pubspec.yaml (environment).
|
||||||
|
FLUTTER_VERSION: '3.47.4'
|
||||||
|
# Static qemu-x86_64 for the ARM64 runner (Debian trixie `qemu-user`, see below).
|
||||||
|
QEMU_DEB_VERSION: '10.0.13+ds-0+deb13u1'
|
||||||
|
QEMU_DEB_SHA1: '02b553a4532192bc999d15408d1e56d7b254906f'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-android:
|
build-android:
|
||||||
name: Build Release APKs
|
name: Build Release APKs
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 60
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
container:
|
container:
|
||||||
image: ghcr.io/cirruslabs/flutter:3.19.3
|
# Cirrus Labs stopped publishing Flutter images in May 2026 (last tag: 3.44.0,
|
||||||
|
# multi-arch amd64/arm64, Ubuntu 24.04, OpenJDK 21, Android SDK 36). The image is
|
||||||
|
# only used for its JDK + Android SDK; the bundled Flutter SDK is switched to
|
||||||
|
# FLUTTER_VERSION in the first step.
|
||||||
|
image: ghcr.io/cirruslabs/flutter:3.44.0
|
||||||
options: --privileged
|
options: --privileged
|
||||||
steps:
|
steps:
|
||||||
|
- name: Switch Flutter SDK to pinned version
|
||||||
|
# act_runner defaults to `sh -e` (dash); bash is needed for `pipefail`.
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
FLUTTER_ROOT="${FLUTTER_ROOT:-/sdks/flutter}"
|
||||||
|
git config --global --add safe.directory "$FLUTTER_ROOT"
|
||||||
|
git config --global --add safe.directory '*'
|
||||||
|
CURRENT="$(git -C "$FLUTTER_ROOT" describe --tags --exact-match 2>/dev/null || true)"
|
||||||
|
if [ "$CURRENT" != "$FLUTTER_VERSION" ]; then
|
||||||
|
echo "Switching Flutter SDK from '${CURRENT:-unknown}' to '$FLUTTER_VERSION'"
|
||||||
|
git -C "$FLUTTER_ROOT" fetch --depth 1 origin "refs/tags/${FLUTTER_VERSION}:refs/tags/${FLUTTER_VERSION}"
|
||||||
|
git -C "$FLUTTER_ROOT" checkout -q "$FLUTTER_VERSION"
|
||||||
|
# Drop the cache of the previous version; the Dart SDK and the engine
|
||||||
|
# artifacts for the new version are downloaded on first use.
|
||||||
|
rm -rf "$FLUTTER_ROOT/bin/cache"
|
||||||
|
fi
|
||||||
|
flutter --version
|
||||||
|
|
||||||
- name: Setup Node.js 20 & ARM64 Flutter Engine
|
- name: Setup Node.js 20 & ARM64 Flutter Engine
|
||||||
run: |
|
run: |
|
||||||
ARCH=$(uname -m)
|
ARCH=$(uname -m)
|
||||||
if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then NODE_ARCH="arm64"; else NODE_ARCH="x64"; fi
|
if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then NODE_ARCH="arm64"; else NODE_ARCH="x64"; fi
|
||||||
curl -fsSL "https://nodejs.org/dist/v20.18.0/node-v20.18.0-linux-${NODE_ARCH}.tar.gz" | tar -xzf - -C /usr/local --strip-components=1
|
curl -fsSL "https://nodejs.org/dist/v20.18.0/node-v20.18.0-linux-${NODE_ARCH}.tar.gz" | tar -xzf - -C /usr/local --strip-components=1
|
||||||
node --version
|
node --version
|
||||||
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
|
||||||
git config --global --add safe.directory '*'
|
|
||||||
|
|
||||||
# On Linux ARM64, Flutter engine and Android AAPT2 need x86_64 emulation via QEMU.
|
# On Linux ARM64, Flutter engine and Android AAPT2 need x86_64 emulation via QEMU.
|
||||||
if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then
|
if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then
|
||||||
echo "Setting up ARM64 Flutter environment & QEMU binfmt emulation..."
|
echo "Setting up ARM64 Flutter environment & QEMU binfmt emulation..."
|
||||||
apt-get update -qq && apt-get install -y -qq qemu-user-static binfmt-support libc6-amd64-cross libstdc++6-amd64-cross gcc
|
apt-get update -qq && apt-get install -y -qq qemu-user-static binfmt-support libc6-amd64-cross libstdc++6-amd64-cross gcc llvm
|
||||||
|
|
||||||
# Setup x86_64 dynamic libraries & prefix for QEMU emulation
|
# Setup x86_64 dynamic libraries & prefix for QEMU emulation
|
||||||
mkdir -p /etc/qemu-binfmt
|
mkdir -p /etc/qemu-binfmt
|
||||||
@@ -49,7 +84,22 @@ jobs:
|
|||||||
export QEMU_LD_PREFIX=/usr/x86_64-linux-gnu
|
export QEMU_LD_PREFIX=/usr/x86_64-linux-gnu
|
||||||
echo "QEMU_LD_PREFIX=/usr/x86_64-linux-gnu" >> "$GITHUB_ENV"
|
echo "QEMU_LD_PREFIX=/usr/x86_64-linux-gnu" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
QEMU_BIN=$(command -v qemu-x86_64-static || command -v qemu-x86_64)
|
# Ubuntu 24.04 ships QEMU 8.2.2, whose x86_64 user-mode emulation dies with
|
||||||
|
# "QEMU internal SIGSEGV {code=MAPERR, addr=0x20}" as soon as the guest calls
|
||||||
|
# pthread_getattr_np() - which Dart's gen_snapshot does since Flutter 3.4x
|
||||||
|
# (https://gitlab.com/qemu-project/qemu/-/issues/2170, fixed in 8.2.3/9.0).
|
||||||
|
# Replace it with the static binary from Debian trixie's qemu-user package.
|
||||||
|
QEMU_DEB="qemu-user_${QEMU_DEB_VERSION}_arm64.deb"
|
||||||
|
curl -fsSL -o /tmp/qemu-user.deb "https://deb.debian.org/debian/pool/main/q/qemu/${QEMU_DEB}" ||
|
||||||
|
curl -fsSL -o /tmp/qemu-user.deb "https://snapshot.debian.org/file/${QEMU_DEB_SHA1}"
|
||||||
|
echo "${QEMU_DEB_SHA1} /tmp/qemu-user.deb" | sha1sum -c -
|
||||||
|
dpkg-deb -x /tmp/qemu-user.deb /tmp/qemu-user
|
||||||
|
install -m 0755 /tmp/qemu-user/usr/bin/qemu-x86_64 /usr/bin/qemu-x86_64-static
|
||||||
|
rm -rf /tmp/qemu-user /tmp/qemu-user.deb
|
||||||
|
/usr/bin/qemu-x86_64-static --version | head -n 1
|
||||||
|
|
||||||
|
# Must match the path hard-coded in scripts/ci/qemu_intercept.c.
|
||||||
|
QEMU_BIN=/usr/bin/qemu-x86_64-static
|
||||||
|
|
||||||
# Dummy fallbacks for FEX binaries/sockets
|
# Dummy fallbacks for FEX binaries/sockets
|
||||||
for fex in /usr/bin/FEXInterpreter /usr/bin/FEX /usr/local/bin/FEXInterpreter /usr/bin/FEXServer /usr/local/bin/FEXServer; do
|
for fex in /usr/bin/FEXInterpreter /usr/bin/FEX /usr/local/bin/FEXInterpreter /usr/bin/FEXServer /usr/local/bin/FEXServer; do
|
||||||
@@ -58,14 +108,31 @@ jobs:
|
|||||||
done
|
done
|
||||||
|
|
||||||
flutter precache --android
|
flutter precache --android
|
||||||
for dir in /sdks/flutter/bin/cache/artifacts/engine/android-*; do
|
# Flutter (still) only ships an x86_64 gen_snapshot for Linux hosts; wrap it
|
||||||
if [ -d "$dir/linux-x64" ]; then
|
# with QEMU under the linux-arm64 path the tool expects.
|
||||||
|
for dir in "${FLUTTER_ROOT:-/sdks/flutter}"/bin/cache/artifacts/engine/android-*; do
|
||||||
|
if [ -d "$dir/linux-x64" ] && [ ! -x "$dir/linux-arm64/gen_snapshot" ]; then
|
||||||
mkdir -p "$dir/linux-arm64"
|
mkdir -p "$dir/linux-arm64"
|
||||||
printf '#!/bin/sh\nSCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"\nexec %s -L /usr/x86_64-linux-gnu "$SCRIPT_DIR/../linux-x64/gen_snapshot" "$@"\n' "$QEMU_BIN" > "$dir/linux-arm64/gen_snapshot"
|
printf '#!/bin/sh\nSCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"\nexec env -u LD_PRELOAD %s -L /usr/x86_64-linux-gnu "$SCRIPT_DIR/../linux-x64/gen_snapshot" "$@"\n' "$QEMU_BIN" > "$dir/linux-arm64/gen_snapshot"
|
||||||
chmod +x "$dir/linux-arm64/gen_snapshot"
|
chmod +x "$dir/linux-arm64/gen_snapshot"
|
||||||
echo "Configured gen_snapshot wrapper for $(basename "$dir")"
|
echo "Configured gen_snapshot wrapper for $(basename "$dir")"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# AGP strips libflutter.so/libapp.so with the NDK's llvm-strip; unstripped they
|
||||||
|
# are ~15x larger (165 MB instead of 12 MB per ABI). The NDK's x86_64 llvm-strip
|
||||||
|
# cannot run under QEMU (needs libz.so.1 etc. in the cross sysroot), so install
|
||||||
|
# the NDK Flutter expects up front and swap in Ubuntu's native arm64 llvm-strip.
|
||||||
|
NDK_VERSION="$(sed -nE 's/.*ndkVersion: String = "([^"]+)".*/\1/p' "${FLUTTER_ROOT:-/sdks/flutter}/packages/flutter_tools/gradle/src/main/kotlin/FlutterExtension.kt")"
|
||||||
|
[ -n "$NDK_VERSION" ] || { echo "::error::Could not determine Flutter's NDK version"; exit 1; }
|
||||||
|
echo "Installing NDK $NDK_VERSION ..."
|
||||||
|
yes | sdkmanager --install "ndk;${NDK_VERSION}" > /dev/null
|
||||||
|
NDK_BIN="${ANDROID_HOME:-/opt/android-sdk-linux}/ndk/${NDK_VERSION}/toolchains/llvm/prebuilt/linux-x86_64/bin"
|
||||||
|
for tool in llvm-strip llvm-objcopy; do
|
||||||
|
mv "$NDK_BIN/$tool" "$NDK_BIN/$tool.x86_64"
|
||||||
|
ln -s "$(command -v "$tool")" "$NDK_BIN/$tool"
|
||||||
|
done
|
||||||
|
"$NDK_BIN/llvm-strip" --version | head -n 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
@@ -102,39 +169,22 @@ jobs:
|
|||||||
flutter analyze
|
flutter analyze
|
||||||
flutter test
|
flutter test
|
||||||
|
|
||||||
- name: Build Universal Release APK
|
- name: Build Release APKs (Split-per-ABI + Universal)
|
||||||
run: |
|
# android/app/build.gradle re-enables the universal APK for split builds, so
|
||||||
flutter build apk --release || {
|
# this one Gradle run yields all four APKs in build/app/outputs/flutter-apk/.
|
||||||
echo "Checking if APK was built despite path mismatch..."
|
run: flutter build apk --release --split-per-abi
|
||||||
find build android -type f -name "*release*.apk" 2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Build Split-per-ABI Release APKs
|
|
||||||
run: |
|
|
||||||
flutter build apk --release --split-per-abi || {
|
|
||||||
echo "Checking if split APKs were built despite path mismatch..."
|
|
||||||
find build android -type f -name "*release*.apk" 2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Organize Artifacts
|
- name: Organize Artifacts
|
||||||
run: |
|
run: |
|
||||||
mkdir -p release-apks
|
mkdir -p release-apks
|
||||||
# Copy universal APK
|
APK_DIR=build/app/outputs/flutter-apk
|
||||||
if [ -f build/app/outputs/flutter-apk/app-release.apk ]; then
|
echo "=== Flutter APK outputs ==="
|
||||||
cp build/app/outputs/flutter-apk/app-release.apk release-apks/oggauer-gemeindetrommler-universal.apk
|
ls -lah "$APK_DIR"
|
||||||
elif [ -f android/app/build/outputs/apk/release/app-release.apk ]; then
|
cp "$APK_DIR/app-release.apk" release-apks/oggauer-gemeindetrommler-universal.apk
|
||||||
cp android/app/build/outputs/apk/release/app-release.apk release-apks/oggauer-gemeindetrommler-universal.apk
|
cp "$APK_DIR/app-arm64-v8a-release.apk" release-apks/oggauer-gemeindetrommler-arm64-v8a.apk
|
||||||
fi
|
cp "$APK_DIR/app-armeabi-v7a-release.apk" release-apks/oggauer-gemeindetrommler-armeabi-v7a.apk
|
||||||
# Copy split APKs
|
cp "$APK_DIR/app-x86_64-release.apk" release-apks/oggauer-gemeindetrommler-x86_64.apk
|
||||||
for apk in build/app/outputs/flutter-apk/*.apk android/app/build/outputs/apk/release/*.apk; do
|
echo "=== Generated APK Artifacts ==="
|
||||||
[ -f "$apk" ] || continue
|
|
||||||
case "$(basename "$apk")" in
|
|
||||||
*arm64-v8a*) cp "$apk" release-apks/oggauer-gemeindetrommler-arm64-v8a.apk ;;
|
|
||||||
*armeabi-v7a*) cp "$apk" release-apks/oggauer-gemeindetrommler-armeabi-v7a.apk ;;
|
|
||||||
*x86_64*) cp "$apk" release-apks/oggauer-gemeindetrommler-x86_64.apk ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
echo "=== Generated All 4 APK Artifacts ==="
|
|
||||||
ls -lah release-apks/
|
ls -lah release-apks/
|
||||||
|
|
||||||
- name: Upload Universal APK
|
- name: Upload Universal APK
|
||||||
@@ -143,6 +193,7 @@ jobs:
|
|||||||
name: oggauer-gemeindetrommler-universal
|
name: oggauer-gemeindetrommler-universal
|
||||||
path: release-apks/oggauer-gemeindetrommler-universal.apk
|
path: release-apks/oggauer-gemeindetrommler-universal.apk
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload ARM64 APK
|
- name: Upload ARM64 APK
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
@@ -150,6 +201,7 @@ jobs:
|
|||||||
name: oggauer-gemeindetrommler-arm64-v8a
|
name: oggauer-gemeindetrommler-arm64-v8a
|
||||||
path: release-apks/oggauer-gemeindetrommler-arm64-v8a.apk
|
path: release-apks/oggauer-gemeindetrommler-arm64-v8a.apk
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload ARMv7 APK
|
- name: Upload ARMv7 APK
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
@@ -157,6 +209,7 @@ jobs:
|
|||||||
name: oggauer-gemeindetrommler-armeabi-v7a
|
name: oggauer-gemeindetrommler-armeabi-v7a
|
||||||
path: release-apks/oggauer-gemeindetrommler-armeabi-v7a.apk
|
path: release-apks/oggauer-gemeindetrommler-armeabi-v7a.apk
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload x86_64 APK
|
- name: Upload x86_64 APK
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
@@ -164,6 +217,7 @@ jobs:
|
|||||||
name: oggauer-gemeindetrommler-x86_64
|
name: oggauer-gemeindetrommler-x86_64
|
||||||
path: release-apks/oggauer-gemeindetrommler-x86_64.apk
|
path: release-apks/oggauer-gemeindetrommler-x86_64.apk
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Publish Direct APKs to Gitea Releases
|
- name: Publish Direct APKs to Gitea Releases
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|||||||
@@ -1,171 +0,0 @@
|
|||||||
name: Build Android APK
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- master
|
|
||||||
- 'refactor/**'
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- master
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-android:
|
|
||||||
name: Build Release APKs
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
container:
|
|
||||||
image: ghcr.io/cirruslabs/flutter:3.19.3
|
|
||||||
options: --privileged
|
|
||||||
steps:
|
|
||||||
- name: Setup Node.js 20 & ARM64 Flutter Engine
|
|
||||||
run: |
|
|
||||||
ARCH=$(uname -m)
|
|
||||||
if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then NODE_ARCH="arm64"; else NODE_ARCH="x64"; fi
|
|
||||||
curl -fsSL "https://nodejs.org/dist/v20.18.0/node-v20.18.0-linux-${NODE_ARCH}.tar.gz" | tar -xzf - -C /usr/local --strip-components=1
|
|
||||||
node --version
|
|
||||||
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
|
||||||
git config --global --add safe.directory '*'
|
|
||||||
|
|
||||||
# On Linux ARM64, Flutter engine and Android AAPT2 need x86_64 emulation via QEMU.
|
|
||||||
if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then
|
|
||||||
echo "Setting up ARM64 Flutter environment & QEMU binfmt emulation..."
|
|
||||||
apt-get update -qq && apt-get install -y -qq qemu-user-static binfmt-support libc6-amd64-cross libstdc++6-amd64-cross gcc
|
|
||||||
|
|
||||||
# Setup x86_64 dynamic libraries & prefix for QEMU emulation
|
|
||||||
mkdir -p /etc/qemu-binfmt
|
|
||||||
ln -sf /usr/x86_64-linux-gnu /etc/qemu-binfmt/x86_64
|
|
||||||
mkdir -p /lib64 /usr/lib64
|
|
||||||
ln -sf /usr/x86_64-linux-gnu/lib/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2
|
|
||||||
ln -sf /usr/x86_64-linux-gnu/lib/* /lib64/ 2>/dev/null || true
|
|
||||||
ln -sf /usr/x86_64-linux-gnu/lib/* /usr/lib64/ 2>/dev/null || true
|
|
||||||
|
|
||||||
export QEMU_LD_PREFIX=/usr/x86_64-linux-gnu
|
|
||||||
echo "QEMU_LD_PREFIX=/usr/x86_64-linux-gnu" >> "$GITHUB_ENV"
|
|
||||||
|
|
||||||
QEMU_BIN=$(command -v qemu-x86_64-static || command -v qemu-x86_64)
|
|
||||||
|
|
||||||
# Dummy fallbacks for FEX binaries/sockets
|
|
||||||
for fex in /usr/bin/FEXInterpreter /usr/bin/FEX /usr/local/bin/FEXInterpreter /usr/bin/FEXServer /usr/local/bin/FEXServer; do
|
|
||||||
printf '#!/bin/sh\nexit 0\n' > "$fex"
|
|
||||||
chmod +x "$fex" 2>/dev/null || true
|
|
||||||
done
|
|
||||||
|
|
||||||
flutter precache --android
|
|
||||||
for dir in /sdks/flutter/bin/cache/artifacts/engine/android-*; do
|
|
||||||
if [ -d "$dir/linux-x64" ]; then
|
|
||||||
mkdir -p "$dir/linux-arm64"
|
|
||||||
printf '#!/bin/sh\nSCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"\nexec %s -L /usr/x86_64-linux-gnu "$SCRIPT_DIR/../linux-x64/gen_snapshot" "$@"\n' "$QEMU_BIN" > "$dir/linux-arm64/gen_snapshot"
|
|
||||||
chmod +x "$dir/linux-arm64/gen_snapshot"
|
|
||||||
echo "Configured gen_snapshot wrapper for $(basename "$dir")"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Checkout Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup QEMU Interceptor
|
|
||||||
run: |
|
|
||||||
ARCH=$(uname -m)
|
|
||||||
if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then
|
|
||||||
echo "Compiling transparent QEMU execution interceptor..."
|
|
||||||
gcc -O2 -shared -fPIC -o /usr/local/lib/libqemu_intercept.so scripts/ci/qemu_intercept.c -ldl
|
|
||||||
echo "LD_PRELOAD=/usr/local/lib/libqemu_intercept.so" >> "$GITHUB_ENV"
|
|
||||||
rm -f /etc/ld.so.preload
|
|
||||||
echo "Enabled LD_PRELOAD interceptor via GITHUB_ENV"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Make Gradlew Executable
|
|
||||||
run: |
|
|
||||||
if [ -f android/gradlew ]; then
|
|
||||||
chmod +x android/gradlew
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Inspect Build Environment
|
|
||||||
run: |
|
|
||||||
flutter --version
|
|
||||||
java -version
|
|
||||||
node --version
|
|
||||||
|
|
||||||
- name: Fetch Flutter Dependencies
|
|
||||||
run: flutter pub get
|
|
||||||
|
|
||||||
- name: Run Code Analysis & Tests
|
|
||||||
run: |
|
|
||||||
flutter analyze
|
|
||||||
flutter test
|
|
||||||
|
|
||||||
- name: Build Universal Release APK
|
|
||||||
run: |
|
|
||||||
flutter build apk --release || {
|
|
||||||
echo "Checking if APK was built despite path mismatch..."
|
|
||||||
find build android -type f -name "*release*.apk" 2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Build Split-per-ABI Release APKs
|
|
||||||
run: |
|
|
||||||
flutter build apk --release --split-per-abi || {
|
|
||||||
echo "Checking if split APKs were built despite path mismatch..."
|
|
||||||
find build android -type f -name "*release*.apk" 2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Organize Artifacts
|
|
||||||
run: |
|
|
||||||
mkdir -p release-apks
|
|
||||||
# Copy universal APK
|
|
||||||
if [ -f build/app/outputs/flutter-apk/app-release.apk ]; then
|
|
||||||
cp build/app/outputs/flutter-apk/app-release.apk release-apks/oggauer-gemeindetrommler-universal.apk
|
|
||||||
elif [ -f android/app/build/outputs/apk/release/app-release.apk ]; then
|
|
||||||
cp android/app/build/outputs/apk/release/app-release.apk release-apks/oggauer-gemeindetrommler-universal.apk
|
|
||||||
fi
|
|
||||||
# Copy split APKs
|
|
||||||
for apk in build/app/outputs/flutter-apk/*.apk android/app/build/outputs/apk/release/*.apk; do
|
|
||||||
[ -f "$apk" ] || continue
|
|
||||||
case "$(basename "$apk")" in
|
|
||||||
*arm64-v8a*) cp "$apk" release-apks/oggauer-gemeindetrommler-arm64-v8a.apk ;;
|
|
||||||
*armeabi-v7a*) cp "$apk" release-apks/oggauer-gemeindetrommler-armeabi-v7a.apk ;;
|
|
||||||
*x86_64*) cp "$apk" release-apks/oggauer-gemeindetrommler-x86_64.apk ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
echo "=== Generated All 4 APK Artifacts ==="
|
|
||||||
ls -lah release-apks/
|
|
||||||
|
|
||||||
- name: Upload Universal APK
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: oggauer-gemeindetrommler-universal
|
|
||||||
path: release-apks/oggauer-gemeindetrommler-universal.apk
|
|
||||||
retention-days: 7
|
|
||||||
|
|
||||||
- name: Upload ARM64 APK
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: oggauer-gemeindetrommler-arm64-v8a
|
|
||||||
path: release-apks/oggauer-gemeindetrommler-arm64-v8a.apk
|
|
||||||
retention-days: 7
|
|
||||||
|
|
||||||
- name: Upload ARMv7 APK
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: oggauer-gemeindetrommler-armeabi-v7a
|
|
||||||
path: release-apks/oggauer-gemeindetrommler-armeabi-v7a.apk
|
|
||||||
retention-days: 7
|
|
||||||
|
|
||||||
- name: Upload x86_64 APK
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: oggauer-gemeindetrommler-x86_64
|
|
||||||
path: release-apks/oggauer-gemeindetrommler-x86_64.apk
|
|
||||||
retention-days: 7
|
|
||||||
|
|
||||||
- name: Clean Workspace Disk Space
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
rm -rf build android/.gradle release-apks 2>/dev/null || true
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Build iOS IPA
|
name: Build iOS IPA
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -10,10 +10,24 @@ on:
|
|||||||
- 'latest'
|
- 'latest'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# Only the newest push per branch/tag is built.
|
||||||
|
concurrency:
|
||||||
|
group: build-ios-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
# Keep in sync with .gitea/workflows/build-apk.yaml and pubspec.yaml (environment).
|
||||||
|
FLUTTER_VERSION: '3.47.4'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-ios:
|
build-ios:
|
||||||
name: Build iOS IPA
|
name: Build iOS IPA
|
||||||
runs-on: macos-14
|
# macOS 26 (arm64) ships Xcode 26 by default; Flutter 3.47 needs Xcode >= 16.
|
||||||
|
runs-on: macos-26
|
||||||
|
timeout-minutes: 60
|
||||||
|
if: github.server_url == 'https://github.com'
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
@@ -22,32 +36,39 @@ jobs:
|
|||||||
- name: Set up Flutter
|
- name: Set up Flutter
|
||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
flutter-version: '3.19.3'
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
channel: 'stable'
|
channel: stable
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
|
- name: Configure Flutter
|
||||||
|
run: |
|
||||||
|
# All plugins of this project are integrated via CocoaPods. Turning
|
||||||
|
# off the (default-on) Swift Package Manager integration keeps the
|
||||||
|
# build on that single, deterministic path.
|
||||||
|
flutter config --no-enable-swift-package-manager
|
||||||
|
|
||||||
- name: Inspect Environment
|
- name: Inspect Environment
|
||||||
run: |
|
run: |
|
||||||
flutter --version
|
|
||||||
xcodebuild -version
|
xcodebuild -version
|
||||||
pod --version
|
pod --version
|
||||||
|
flutter --version
|
||||||
|
flutter doctor -v
|
||||||
|
|
||||||
- name: Fetch Dependencies
|
- name: Fetch Flutter Dependencies
|
||||||
run: flutter pub get
|
run: flutter pub get
|
||||||
|
|
||||||
- name: Install CocoaPods Pods
|
- name: Build iOS (Unsigned)
|
||||||
run: |
|
run: flutter build ios --release --no-codesign
|
||||||
cd ios
|
|
||||||
pod install --repo-update
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
- name: Build iOS (Unsigned IPA)
|
- name: Package IPA
|
||||||
run: |
|
run: |
|
||||||
flutter build ios --release --no-codesign
|
set -euo pipefail
|
||||||
mkdir -p build/ios/iphoneos/Payload
|
|
||||||
cp -r build/ios/iphoneos/Runner.app build/ios/iphoneos/Payload/
|
|
||||||
cd build/ios/iphoneos
|
cd build/ios/iphoneos
|
||||||
zip -r oggauer-gemeindetrommler.ipa Payload
|
rm -rf Payload oggauer-gemeindetrommler.ipa
|
||||||
|
mkdir Payload
|
||||||
|
cp -R Runner.app Payload/
|
||||||
|
zip -qry oggauer-gemeindetrommler.ipa Payload
|
||||||
|
ls -lh oggauer-gemeindetrommler.ipa
|
||||||
|
|
||||||
- name: Upload IPA Artifact
|
- name: Upload IPA Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
@@ -55,3 +76,27 @@ jobs:
|
|||||||
name: oggauer-gemeindetrommler-ios
|
name: oggauer-gemeindetrommler-ios
|
||||||
path: build/ios/iphoneos/oggauer-gemeindetrommler.ipa
|
path: build/ios/iphoneos/oggauer-gemeindetrommler.ipa
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Publish IPA to GitHub Release
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || 'latest' }}
|
||||||
|
name: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || 'Oggauer Gemeindetrommler - LATEST' }}
|
||||||
|
files: build/ios/iphoneos/oggauer-gemeindetrommler.ipa
|
||||||
|
make_latest: true
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Upload Xcode Logs (on failure)
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ios-build-logs
|
||||||
|
path: |
|
||||||
|
~/Library/Logs/DiagnosticReports/*.ips
|
||||||
|
build/ios/**/*.log
|
||||||
|
ios/Podfile.lock
|
||||||
|
if-no-files-found: ignore
|
||||||
|
retention-days: 3
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ Die Sicherheit und Integrität der Bürgerdaten wird durch ein mehrstufiges Sich
|
|||||||
## 🏗️ Technische Architektur & Tech-Stack
|
## 🏗️ Technische Architektur & Tech-Stack
|
||||||
|
|
||||||
### Verwendete Technologien:
|
### Verwendete Technologien:
|
||||||
- **Framework:** [Flutter](https://flutter.dev) (Dart SDK `>=3.0.0`)
|
- **Framework:** [Flutter](https://flutter.dev) `3.47.x` (Dart SDK `>=3.11`), Android `minSdk 23`, iOS `>= 15.0`
|
||||||
- **Backend & Cloud Services (Google Firebase):**
|
- **Backend & Cloud Services (Google Firebase):**
|
||||||
- `firebase_core` (Initialisierung)
|
- `firebase_core` (Initialisierung)
|
||||||
- `firebase_auth` (E-Mail & Passwort Authentifizierung, Gast-Modus)
|
- `firebase_auth` (E-Mail & Passwort Authentifizierung, Gast-Modus)
|
||||||
@@ -117,7 +117,7 @@ Die Sicherheit und Integrität der Bürgerdaten wird durch ein mehrstufiges Sich
|
|||||||
- `pdf`, `printing` (PDF-Generierung, In-App Vorschau & Druck)
|
- `pdf`, `printing` (PDF-Generierung, In-App Vorschau & Druck)
|
||||||
- `url_launcher` (System-Browser, Telefon- und E-Mail-Intents)
|
- `url_launcher` (System-Browser, Telefon- und E-Mail-Intents)
|
||||||
- **Bildverarbeitung:**
|
- **Bildverarbeitung:**
|
||||||
- `image_picker`, `image_cropper`, `cached_network_image`, `image`
|
- `image_picker`, `cached_network_image`, `image`
|
||||||
- **State Management:**
|
- **State Management:**
|
||||||
- `provider` (`MultiProvider` Architektur)
|
- `provider` (`MultiProvider` Architektur)
|
||||||
|
|
||||||
@@ -150,25 +150,34 @@ lib/
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🤖 CI/CD Pipeline (Gitea Actions)
|
## 🤖 CI/CD Pipeline
|
||||||
|
|
||||||
Die App verfügt über eine vollautomatische Build-Pipeline für **Gitea Actions** (`.gitea/workflows/build-apk.yaml`):
|
Die Flutter-Version ist zentral gepinnt (`FLUTTER_VERSION` in beiden Workflows, `environment` in `pubspec.yaml`) und muss bei einem Upgrade an allen drei Stellen gemeinsam angehoben werden.
|
||||||
|
|
||||||
- **Build-Container:** `ghcr.io/cirruslabs/flutter:3.19.3` mit vorkonfiguriertem Android SDK und Java 17.
|
### Android (Gitea Actions, `.gitea/workflows/build-apk.yaml`)
|
||||||
|
|
||||||
|
- **Build-Container:** `ghcr.io/cirruslabs/flutter:3.44.0` (letztes von Cirrus Labs veröffentlichtes Image, liefert JDK 21 + Android SDK 36). Das darin enthaltene Flutter-SDK wird im ersten Schritt per `git checkout` auf `FLUTTER_VERSION` umgeschaltet.
|
||||||
|
- **Runner:** Oracle Cloud ARM64 – x86_64-Werkzeuge (AAPT2, `gen_snapshot`) laufen über QEMU (`scripts/ci/qemu_intercept.c`).
|
||||||
- **Automatisierte Qualitätskontrolle:** Führt vor jedem Build `flutter analyze` und `flutter test` aus.
|
- **Automatisierte Qualitätskontrolle:** Führt vor jedem Build `flutter analyze` und `flutter test` aus.
|
||||||
- **Erzeugte APK-Pakete:**
|
- **Erzeugte APK-Pakete:**
|
||||||
1. `oggauer-gemeindetrommler-universal-release.apk` (Universelle Version für alle Android-Smartphones).
|
1. `oggauer-gemeindetrommler-universal-release.apk` (Universelle Version für alle Android-Smartphones).
|
||||||
2. `oggauer-gemeindetrommler-arm64-v8a.apk` (Optimiert für moderne 64-Bit Geräte).
|
2. `oggauer-gemeindetrommler-arm64-v8a.apk` (Optimiert für moderne 64-Bit Geräte).
|
||||||
3. `oggauer-gemeindetrommler-armeabi-v7a.apk` (Für ältere 32-Bit Geräte).
|
3. `oggauer-gemeindetrommler-armeabi-v7a.apk` (Für ältere 32-Bit Geräte).
|
||||||
4. `oggauer-gemeindetrommler-x86_64.apk` (Für Emulatoren).
|
4. `oggauer-gemeindetrommler-x86_64.apk` (Für Emulatoren).
|
||||||
- **Download:** Die gebauten APKs stehen nach jedem Durchlauf als ZIP-Artefakt (`oggauer-gemeindetrommler-apks`) im Gitea-Reiter **Aktionen** zum Download bereit.
|
- **Download:** Die gebauten APKs stehen nach jedem Durchlauf als Artefakte im Gitea-Reiter **Aktionen** sowie im Release `latest` zum Download bereit.
|
||||||
|
|
||||||
|
### iOS (GitHub Actions, `.github/workflows/build-ios.yaml`)
|
||||||
|
|
||||||
|
- **Runner:** `macos-26` (Xcode 26), Flutter über `subosito/flutter-action`.
|
||||||
|
- **Plugins:** ausschließlich über CocoaPods (Swift Package Manager ist im Workflow bewusst deaktiviert); `ios/Podfile.lock` wird nicht versioniert, `pod install` läuft bei jedem Build frisch.
|
||||||
|
- **Ergebnis:** unsigniertes IPA (`flutter build ios --release --no-codesign`) als Artefakt `oggauer-gemeindetrommler-ios` – für die Verteilung muss es nachträglich signiert werden.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🛠️ Einrichtung & Lokale Ausführung
|
## 🛠️ Einrichtung & Lokale Ausführung
|
||||||
|
|
||||||
### Voraussetzungen:
|
### Voraussetzungen:
|
||||||
- **Flutter SDK:** `>= 3.19.x`
|
- **Flutter SDK:** `3.47.x` (stable) – identisch zur in den Workflows gepinnten Version
|
||||||
- **Dart SDK:** Im Flutter SDK enthalten
|
- **Dart SDK:** Im Flutter SDK enthalten
|
||||||
- **Android Studio / VS Code / Xcode** mit entsprechenden Emulatoren oder Testgeräten
|
- **Android Studio / VS Code / Xcode** mit entsprechenden Emulatoren oder Testgeräten
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,12 @@
|
|||||||
# packages, and plugins designed to encourage good coding practices.
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
include: package:flutter_lints/flutter.yaml
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
analyzer:
|
||||||
|
exclude:
|
||||||
|
- build/**
|
||||||
|
- android/**
|
||||||
|
- ios/**
|
||||||
|
|
||||||
linter:
|
linter:
|
||||||
# The lint rules applied to this project can be customized in the
|
# The lint rules applied to this project can be customized in the
|
||||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||||
|
|||||||
+34
-39
@@ -1,60 +1,34 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id "com.android.application"
|
id "com.android.application"
|
||||||
id "kotlin-android"
|
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||||
|
// (With android.builtInKotlin=false the Flutter plugin applies kotlin-android itself.)
|
||||||
id "dev.flutter.flutter-gradle-plugin"
|
id "dev.flutter.flutter-gradle-plugin"
|
||||||
id "com.google.gms.google-services"
|
id "com.google.gms.google-services"
|
||||||
}
|
}
|
||||||
|
|
||||||
def localProperties = new Properties()
|
|
||||||
def localPropertiesFile = rootProject.file('local.properties')
|
|
||||||
if (localPropertiesFile.exists()) {
|
|
||||||
localPropertiesFile.withReader('UTF-8') { reader ->
|
|
||||||
localProperties.load(reader)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
|
||||||
if (flutterVersionCode == null) {
|
|
||||||
flutterVersionCode = '1'
|
|
||||||
}
|
|
||||||
|
|
||||||
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
|
||||||
if (flutterVersionName == null) {
|
|
||||||
flutterVersionName = '1.0'
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace "com.oggauergemeindetrommler.app"
|
namespace "com.oggauergemeindetrommler.app"
|
||||||
compileSdkVersion flutter.compileSdkVersion
|
compileSdk flutter.compileSdkVersion
|
||||||
ndkVersion "26.1.10909125"
|
ndkVersion flutter.ndkVersion
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_17
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
targetCompatibility JavaVersion.VERSION_17
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = '17'
|
|
||||||
}
|
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
main.java.srcDirs += 'src/main/kotlin'
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
|
||||||
applicationId "com.oggauergemeindetrommler.app"
|
applicationId "com.oggauergemeindetrommler.app"
|
||||||
// You can update the following values to match your application needs.
|
// Flutter 3.47 defaults to 24; 23 (Android 6.0) is still supported and
|
||||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
|
// is also the minimum required by the current Firebase Android SDKs.
|
||||||
minSdkVersion 23
|
minSdkVersion flutter.minSdkVersion
|
||||||
targetSdkVersion flutter.targetSdkVersion
|
targetSdk flutter.targetSdkVersion
|
||||||
versionCode flutterVersionCode.toInteger()
|
// Read from pubspec.yaml (via local.properties); defaults to 1 / "1.0".
|
||||||
versionName flutterVersionName
|
versionCode flutter.versionCode
|
||||||
multiDexEnabled true
|
versionName flutter.versionName
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -64,10 +38,31 @@ android {
|
|||||||
signingConfig signingConfigs.debug
|
signingConfig signingConfigs.debug
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `flutter build apk --split-per-abi` makes the Flutter Gradle plugin enable ABI
|
||||||
|
// splits for the requested target platforms but switch the universal APK off.
|
||||||
|
// Turn it back on so a single Gradle run yields the per-ABI APKs *and* the
|
||||||
|
// universal one (the plugin copies all of them to build/app/outputs/flutter-apk/,
|
||||||
|
// the universal one as app-release.apk). Only when splitting: for plain builds
|
||||||
|
// the plugin sets ndk.abiFilters instead, which AGP does not allow together
|
||||||
|
// with splits. Do not use AGP's `packageReleaseUniversalApk` task for this -
|
||||||
|
// it derives the universal APK from an App Bundle, and bundles cannot be
|
||||||
|
// built while ABI splits are enabled.
|
||||||
|
if (project.findProperty("split-per-abi")?.toString()?.toBoolean()) {
|
||||||
|
splits {
|
||||||
|
abi {
|
||||||
|
universalApk true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flutter {
|
flutter {
|
||||||
source '../..'
|
source '../..'
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {}
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
package="com.oggauergemeindetrommler.app">
|
|
||||||
<application
|
<application
|
||||||
android:label="Oggauer Gemeindetrommler"
|
android:label="Oggauer Gemeindetrommler"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
|
|||||||
-6
@@ -1,6 +0,0 @@
|
|||||||
package com.oggauergemeindetrommler.oggauer_gemeindetrommler
|
|
||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
|
||||||
|
|
||||||
class MainActivity: FlutterActivity() {
|
|
||||||
}
|
|
||||||
+7
-18
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
@@ -6,28 +5,18 @@ allprojects {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rootProject.buildDir = '../build'
|
// Keep all Gradle outputs under <repo>/build so the Flutter CLI finds the APKs.
|
||||||
subprojects {
|
// (Project.buildDir was removed in Gradle 9 -> use layout.buildDirectory.)
|
||||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
def newBuildDir = rootProject.layout.buildDirectory.dir("../../build").get()
|
||||||
}
|
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||||
subprojects {
|
|
||||||
|
|
||||||
// Inject namespace into plugins that don't have it (required for AGP 8+)
|
subprojects {
|
||||||
afterEvaluate { project ->
|
project.layout.buildDirectory.value(newBuildDir.dir(project.name))
|
||||||
if (project.hasProperty('android')) {
|
|
||||||
project.android {
|
|
||||||
if (namespace == null) {
|
|
||||||
namespace project.group
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
subprojects {
|
subprojects {
|
||||||
project.evaluationDependsOn(':app')
|
project.evaluationDependsOn(':app')
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register("clean", Delete) {
|
tasks.register("clean", Delete) {
|
||||||
delete rootProject.buildDir
|
delete rootProject.layout.buildDirectory
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M -Djdk.lang.Process.launchMechanism=vfork
|
# vfork keeps process spawning cheap under QEMU user-mode emulation (ARM64 CI runner).
|
||||||
|
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=1G -XX:+HeapDumpOnOutOfMemoryError -Djdk.lang.Process.launchMechanism=vfork
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
# Stay on the legacy AGP DSL / external Kotlin plugin until all Flutter plugins
|
||||||
|
# support the AGP 9 built-in Kotlin (same as the Flutter 3.47 project template).
|
||||||
|
android.newDsl=false
|
||||||
|
android.builtInKotlin=false
|
||||||
|
# Kotlin incremental compilation stores project-relative paths and breaks on Windows
|
||||||
|
# when the pub cache (C:) and the project live on different drives
|
||||||
|
# ("this and base files have different roots"). CI builds are clean anyway.
|
||||||
|
kotlin.incremental=false
|
||||||
|
|||||||
+1
-1
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-all.zip
|
||||||
|
|||||||
@@ -20,11 +20,14 @@ pluginManagement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Versions follow the Flutter 3.47 project template (see
|
||||||
|
// packages/flutter_tools/lib/src/android/gradle_utils.dart in the Flutter SDK).
|
||||||
|
// Flutter refuses to build with AGP < 8.11.1, Kotlin < 2.2.20 or Gradle < 8.14.
|
||||||
plugins {
|
plugins {
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
id "com.android.application" version "8.5.2" apply false
|
id "com.android.application" version "9.1.0" apply false
|
||||||
id "org.jetbrains.kotlin.android" version "1.9.22" apply false
|
id "org.jetbrains.kotlin.android" version "2.4.0" apply false
|
||||||
id "com.google.gms.google-services" version "4.4.1" apply false
|
id "com.google.gms.google-services" version "4.5.0" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
include ":app"
|
include ":app"
|
||||||
|
|||||||
@@ -32,3 +32,8 @@ Runner/GeneratedPluginRegistrant.*
|
|||||||
!default.mode2v3
|
!default.mode2v3
|
||||||
!default.pbxuser
|
!default.pbxuser
|
||||||
!default.perspectivev3
|
!default.perspectivev3
|
||||||
|
|
||||||
|
# Podfile.lock is intentionally not tracked: development happens on Windows (no
|
||||||
|
# CocoaPods), and the iOS CI always runs a fresh `pod install`. A stale lock file
|
||||||
|
# only causes "sandbox is not in sync" / incompatible pod version errors.
|
||||||
|
Podfile.lock
|
||||||
|
|||||||
@@ -20,7 +20,5 @@
|
|||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1.0</string>
|
<string>1.0</string>
|
||||||
<key>MinimumOSVersion</key>
|
|
||||||
<string>11.0</string>
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -1,2 +1,5 @@
|
|||||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||||
#include "Generated.xcconfig"
|
#include "Generated.xcconfig"
|
||||||
|
CODE_SIGNING_ALLOWED=NO
|
||||||
|
CODE_SIGNING_REQUIRED=NO
|
||||||
|
CODE_SIGN_IDENTITY=
|
||||||
|
|||||||
+10
-2
@@ -1,5 +1,5 @@
|
|||||||
# Uncomment this line to define a global platform for your project
|
# Flutter 3.47+ requires iOS 15.0 as the minimum deployment target.
|
||||||
# platform :ios, '12.0'
|
platform :ios, '15.0'
|
||||||
|
|
||||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
@@ -37,5 +37,13 @@ end
|
|||||||
post_install do |installer|
|
post_install do |installer|
|
||||||
installer.pods_project.targets.each do |target|
|
installer.pods_project.targets.each do |target|
|
||||||
flutter_additional_ios_build_settings(target)
|
flutter_additional_ios_build_settings(target)
|
||||||
|
target.build_configurations.each do |config|
|
||||||
|
# Keep every pod on the same deployment target as the app so old
|
||||||
|
# podspecs do not drag the minimum back below what Flutter supports.
|
||||||
|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
|
||||||
|
# Pods never need to be signed; the CI produces an unsigned IPA.
|
||||||
|
config.build_settings['CODE_SIGNING_REQUIRED'] = 'NO'
|
||||||
|
config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,842 +0,0 @@
|
|||||||
PODS:
|
|
||||||
- abseil/algorithm (1.20211102.0):
|
|
||||||
- abseil/algorithm/algorithm (= 1.20211102.0)
|
|
||||||
- abseil/algorithm/container (= 1.20211102.0)
|
|
||||||
- abseil/algorithm/algorithm (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/algorithm/container (1.20211102.0):
|
|
||||||
- abseil/algorithm/algorithm
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/meta/type_traits
|
|
||||||
- abseil/base (1.20211102.0):
|
|
||||||
- abseil/base/atomic_hook (= 1.20211102.0)
|
|
||||||
- abseil/base/base (= 1.20211102.0)
|
|
||||||
- abseil/base/base_internal (= 1.20211102.0)
|
|
||||||
- abseil/base/config (= 1.20211102.0)
|
|
||||||
- abseil/base/core_headers (= 1.20211102.0)
|
|
||||||
- abseil/base/dynamic_annotations (= 1.20211102.0)
|
|
||||||
- abseil/base/endian (= 1.20211102.0)
|
|
||||||
- abseil/base/errno_saver (= 1.20211102.0)
|
|
||||||
- abseil/base/fast_type_id (= 1.20211102.0)
|
|
||||||
- abseil/base/log_severity (= 1.20211102.0)
|
|
||||||
- abseil/base/malloc_internal (= 1.20211102.0)
|
|
||||||
- abseil/base/pretty_function (= 1.20211102.0)
|
|
||||||
- abseil/base/raw_logging_internal (= 1.20211102.0)
|
|
||||||
- abseil/base/spinlock_wait (= 1.20211102.0)
|
|
||||||
- abseil/base/strerror (= 1.20211102.0)
|
|
||||||
- abseil/base/throw_delegate (= 1.20211102.0)
|
|
||||||
- abseil/base/atomic_hook (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/base/base (1.20211102.0):
|
|
||||||
- abseil/base/atomic_hook
|
|
||||||
- abseil/base/base_internal
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/base/dynamic_annotations
|
|
||||||
- abseil/base/log_severity
|
|
||||||
- abseil/base/raw_logging_internal
|
|
||||||
- abseil/base/spinlock_wait
|
|
||||||
- abseil/meta/type_traits
|
|
||||||
- abseil/base/base_internal (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/meta/type_traits
|
|
||||||
- abseil/base/config (1.20211102.0)
|
|
||||||
- abseil/base/core_headers (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/dynamic_annotations (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/base/endian (1.20211102.0):
|
|
||||||
- abseil/base/base
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/base/errno_saver (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/fast_type_id (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/log_severity (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/base/malloc_internal (1.20211102.0):
|
|
||||||
- abseil/base/base
|
|
||||||
- abseil/base/base_internal
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/base/dynamic_annotations
|
|
||||||
- abseil/base/raw_logging_internal
|
|
||||||
- abseil/base/pretty_function (1.20211102.0)
|
|
||||||
- abseil/base/raw_logging_internal (1.20211102.0):
|
|
||||||
- abseil/base/atomic_hook
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/base/log_severity
|
|
||||||
- abseil/base/spinlock_wait (1.20211102.0):
|
|
||||||
- abseil/base/base_internal
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/base/errno_saver
|
|
||||||
- abseil/base/strerror (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/base/errno_saver
|
|
||||||
- abseil/base/throw_delegate (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/raw_logging_internal
|
|
||||||
- abseil/container/common (1.20211102.0):
|
|
||||||
- abseil/meta/type_traits
|
|
||||||
- abseil/types/optional
|
|
||||||
- abseil/container/compressed_tuple (1.20211102.0):
|
|
||||||
- abseil/utility/utility
|
|
||||||
- abseil/container/container_memory (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/memory/memory
|
|
||||||
- abseil/meta/type_traits
|
|
||||||
- abseil/utility/utility
|
|
||||||
- abseil/container/fixed_array (1.20211102.0):
|
|
||||||
- abseil/algorithm/algorithm
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/base/dynamic_annotations
|
|
||||||
- abseil/base/throw_delegate
|
|
||||||
- abseil/container/compressed_tuple
|
|
||||||
- abseil/memory/memory
|
|
||||||
- abseil/container/flat_hash_map (1.20211102.0):
|
|
||||||
- abseil/algorithm/container
|
|
||||||
- abseil/container/container_memory
|
|
||||||
- abseil/container/hash_function_defaults
|
|
||||||
- abseil/container/raw_hash_map
|
|
||||||
- abseil/memory/memory
|
|
||||||
- abseil/container/hash_function_defaults (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/hash/hash
|
|
||||||
- abseil/strings/cord
|
|
||||||
- abseil/strings/strings
|
|
||||||
- abseil/container/hash_policy_traits (1.20211102.0):
|
|
||||||
- abseil/meta/type_traits
|
|
||||||
- abseil/container/hashtable_debug_hooks (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/container/hashtablez_sampler (1.20211102.0):
|
|
||||||
- abseil/base/base
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/container/have_sse
|
|
||||||
- abseil/debugging/stacktrace
|
|
||||||
- abseil/memory/memory
|
|
||||||
- abseil/profiling/exponential_biased
|
|
||||||
- abseil/profiling/sample_recorder
|
|
||||||
- abseil/synchronization/synchronization
|
|
||||||
- abseil/utility/utility
|
|
||||||
- abseil/container/have_sse (1.20211102.0)
|
|
||||||
- abseil/container/inlined_vector (1.20211102.0):
|
|
||||||
- abseil/algorithm/algorithm
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/base/throw_delegate
|
|
||||||
- abseil/container/inlined_vector_internal
|
|
||||||
- abseil/memory/memory
|
|
||||||
- abseil/container/inlined_vector_internal (1.20211102.0):
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/container/compressed_tuple
|
|
||||||
- abseil/memory/memory
|
|
||||||
- abseil/meta/type_traits
|
|
||||||
- abseil/types/span
|
|
||||||
- abseil/container/layout (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/meta/type_traits
|
|
||||||
- abseil/strings/strings
|
|
||||||
- abseil/types/span
|
|
||||||
- abseil/utility/utility
|
|
||||||
- abseil/container/raw_hash_map (1.20211102.0):
|
|
||||||
- abseil/base/throw_delegate
|
|
||||||
- abseil/container/container_memory
|
|
||||||
- abseil/container/raw_hash_set
|
|
||||||
- abseil/container/raw_hash_set (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/base/endian
|
|
||||||
- abseil/container/common
|
|
||||||
- abseil/container/compressed_tuple
|
|
||||||
- abseil/container/container_memory
|
|
||||||
- abseil/container/hash_policy_traits
|
|
||||||
- abseil/container/hashtable_debug_hooks
|
|
||||||
- abseil/container/hashtablez_sampler
|
|
||||||
- abseil/container/have_sse
|
|
||||||
- abseil/memory/memory
|
|
||||||
- abseil/meta/type_traits
|
|
||||||
- abseil/numeric/bits
|
|
||||||
- abseil/utility/utility
|
|
||||||
- abseil/debugging/debugging_internal (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/base/dynamic_annotations
|
|
||||||
- abseil/base/errno_saver
|
|
||||||
- abseil/base/raw_logging_internal
|
|
||||||
- abseil/debugging/demangle_internal (1.20211102.0):
|
|
||||||
- abseil/base/base
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/debugging/stacktrace (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/debugging/debugging_internal
|
|
||||||
- abseil/debugging/symbolize (1.20211102.0):
|
|
||||||
- abseil/base/base
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/base/dynamic_annotations
|
|
||||||
- abseil/base/malloc_internal
|
|
||||||
- abseil/base/raw_logging_internal
|
|
||||||
- abseil/debugging/debugging_internal
|
|
||||||
- abseil/debugging/demangle_internal
|
|
||||||
- abseil/strings/strings
|
|
||||||
- abseil/functional/bind_front (1.20211102.0):
|
|
||||||
- abseil/base/base_internal
|
|
||||||
- abseil/container/compressed_tuple
|
|
||||||
- abseil/meta/type_traits
|
|
||||||
- abseil/utility/utility
|
|
||||||
- abseil/functional/function_ref (1.20211102.0):
|
|
||||||
- abseil/base/base_internal
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/meta/type_traits
|
|
||||||
- abseil/hash/city (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/base/endian
|
|
||||||
- abseil/hash/hash (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/base/endian
|
|
||||||
- abseil/container/fixed_array
|
|
||||||
- abseil/hash/city
|
|
||||||
- abseil/hash/low_level_hash
|
|
||||||
- abseil/meta/type_traits
|
|
||||||
- abseil/numeric/int128
|
|
||||||
- abseil/strings/strings
|
|
||||||
- abseil/types/optional
|
|
||||||
- abseil/types/variant
|
|
||||||
- abseil/utility/utility
|
|
||||||
- abseil/hash/low_level_hash (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/endian
|
|
||||||
- abseil/numeric/bits
|
|
||||||
- abseil/numeric/int128
|
|
||||||
- abseil/memory (1.20211102.0):
|
|
||||||
- abseil/memory/memory (= 1.20211102.0)
|
|
||||||
- abseil/memory/memory (1.20211102.0):
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/meta/type_traits
|
|
||||||
- abseil/meta (1.20211102.0):
|
|
||||||
- abseil/meta/type_traits (= 1.20211102.0)
|
|
||||||
- abseil/meta/type_traits (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/numeric/bits (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/numeric/int128 (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/numeric/bits
|
|
||||||
- abseil/numeric/representation (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/profiling/exponential_biased (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/profiling/sample_recorder (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/synchronization/synchronization
|
|
||||||
- abseil/time/time
|
|
||||||
- abseil/random/distributions (1.20211102.0):
|
|
||||||
- abseil/base/base_internal
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/meta/type_traits
|
|
||||||
- abseil/numeric/bits
|
|
||||||
- abseil/random/internal/distribution_caller
|
|
||||||
- abseil/random/internal/fast_uniform_bits
|
|
||||||
- abseil/random/internal/fastmath
|
|
||||||
- abseil/random/internal/generate_real
|
|
||||||
- abseil/random/internal/iostream_state_saver
|
|
||||||
- abseil/random/internal/traits
|
|
||||||
- abseil/random/internal/uniform_helper
|
|
||||||
- abseil/random/internal/wide_multiply
|
|
||||||
- abseil/strings/strings
|
|
||||||
- abseil/random/internal/distribution_caller (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/fast_type_id
|
|
||||||
- abseil/utility/utility
|
|
||||||
- abseil/random/internal/fast_uniform_bits (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/meta/type_traits
|
|
||||||
- abseil/random/internal/fastmath (1.20211102.0):
|
|
||||||
- abseil/numeric/bits
|
|
||||||
- abseil/random/internal/generate_real (1.20211102.0):
|
|
||||||
- abseil/meta/type_traits
|
|
||||||
- abseil/numeric/bits
|
|
||||||
- abseil/random/internal/fastmath
|
|
||||||
- abseil/random/internal/traits
|
|
||||||
- abseil/random/internal/iostream_state_saver (1.20211102.0):
|
|
||||||
- abseil/meta/type_traits
|
|
||||||
- abseil/numeric/int128
|
|
||||||
- abseil/random/internal/nonsecure_base (1.20211102.0):
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/meta/type_traits
|
|
||||||
- abseil/random/internal/pool_urbg
|
|
||||||
- abseil/random/internal/salted_seed_seq
|
|
||||||
- abseil/random/internal/seed_material
|
|
||||||
- abseil/types/optional
|
|
||||||
- abseil/types/span
|
|
||||||
- abseil/random/internal/pcg_engine (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/meta/type_traits
|
|
||||||
- abseil/numeric/bits
|
|
||||||
- abseil/numeric/int128
|
|
||||||
- abseil/random/internal/fastmath
|
|
||||||
- abseil/random/internal/iostream_state_saver
|
|
||||||
- abseil/random/internal/platform (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/random/internal/pool_urbg (1.20211102.0):
|
|
||||||
- abseil/base/base
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/base/endian
|
|
||||||
- abseil/base/raw_logging_internal
|
|
||||||
- abseil/random/internal/randen
|
|
||||||
- abseil/random/internal/seed_material
|
|
||||||
- abseil/random/internal/traits
|
|
||||||
- abseil/random/seed_gen_exception
|
|
||||||
- abseil/types/span
|
|
||||||
- abseil/random/internal/randen (1.20211102.0):
|
|
||||||
- abseil/base/raw_logging_internal
|
|
||||||
- abseil/random/internal/platform
|
|
||||||
- abseil/random/internal/randen_hwaes
|
|
||||||
- abseil/random/internal/randen_slow
|
|
||||||
- abseil/random/internal/randen_engine (1.20211102.0):
|
|
||||||
- abseil/base/endian
|
|
||||||
- abseil/meta/type_traits
|
|
||||||
- abseil/random/internal/iostream_state_saver
|
|
||||||
- abseil/random/internal/randen
|
|
||||||
- abseil/random/internal/randen_hwaes (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/random/internal/platform
|
|
||||||
- abseil/random/internal/randen_hwaes_impl
|
|
||||||
- abseil/random/internal/randen_hwaes_impl (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/numeric/int128
|
|
||||||
- abseil/random/internal/platform
|
|
||||||
- abseil/random/internal/randen_slow (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/base/endian
|
|
||||||
- abseil/numeric/int128
|
|
||||||
- abseil/random/internal/platform
|
|
||||||
- abseil/random/internal/salted_seed_seq (1.20211102.0):
|
|
||||||
- abseil/container/inlined_vector
|
|
||||||
- abseil/meta/type_traits
|
|
||||||
- abseil/random/internal/seed_material
|
|
||||||
- abseil/types/optional
|
|
||||||
- abseil/types/span
|
|
||||||
- abseil/random/internal/seed_material (1.20211102.0):
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/base/dynamic_annotations
|
|
||||||
- abseil/base/raw_logging_internal
|
|
||||||
- abseil/random/internal/fast_uniform_bits
|
|
||||||
- abseil/strings/strings
|
|
||||||
- abseil/types/optional
|
|
||||||
- abseil/types/span
|
|
||||||
- abseil/random/internal/traits (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/random/internal/uniform_helper (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/meta/type_traits
|
|
||||||
- abseil/random/internal/traits
|
|
||||||
- abseil/random/internal/wide_multiply (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/numeric/bits
|
|
||||||
- abseil/numeric/int128
|
|
||||||
- abseil/random/internal/traits
|
|
||||||
- abseil/random/random (1.20211102.0):
|
|
||||||
- abseil/random/distributions
|
|
||||||
- abseil/random/internal/nonsecure_base
|
|
||||||
- abseil/random/internal/pcg_engine
|
|
||||||
- abseil/random/internal/pool_urbg
|
|
||||||
- abseil/random/internal/randen_engine
|
|
||||||
- abseil/random/seed_sequences
|
|
||||||
- abseil/random/seed_gen_exception (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/random/seed_sequences (1.20211102.0):
|
|
||||||
- abseil/container/inlined_vector
|
|
||||||
- abseil/random/internal/nonsecure_base
|
|
||||||
- abseil/random/internal/pool_urbg
|
|
||||||
- abseil/random/internal/salted_seed_seq
|
|
||||||
- abseil/random/internal/seed_material
|
|
||||||
- abseil/random/seed_gen_exception
|
|
||||||
- abseil/types/span
|
|
||||||
- abseil/status/status (1.20211102.0):
|
|
||||||
- abseil/base/atomic_hook
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/base/raw_logging_internal
|
|
||||||
- abseil/container/inlined_vector
|
|
||||||
- abseil/debugging/stacktrace
|
|
||||||
- abseil/debugging/symbolize
|
|
||||||
- abseil/functional/function_ref
|
|
||||||
- abseil/strings/cord
|
|
||||||
- abseil/strings/str_format
|
|
||||||
- abseil/strings/strings
|
|
||||||
- abseil/types/optional
|
|
||||||
- abseil/status/statusor (1.20211102.0):
|
|
||||||
- abseil/base/base
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/base/raw_logging_internal
|
|
||||||
- abseil/meta/type_traits
|
|
||||||
- abseil/status/status
|
|
||||||
- abseil/strings/strings
|
|
||||||
- abseil/types/variant
|
|
||||||
- abseil/utility/utility
|
|
||||||
- abseil/strings/cord (1.20211102.0):
|
|
||||||
- abseil/base/base
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/base/endian
|
|
||||||
- abseil/base/raw_logging_internal
|
|
||||||
- abseil/container/fixed_array
|
|
||||||
- abseil/container/inlined_vector
|
|
||||||
- abseil/functional/function_ref
|
|
||||||
- abseil/meta/type_traits
|
|
||||||
- abseil/strings/cord_internal
|
|
||||||
- abseil/strings/cordz_functions
|
|
||||||
- abseil/strings/cordz_info
|
|
||||||
- abseil/strings/cordz_statistics
|
|
||||||
- abseil/strings/cordz_update_scope
|
|
||||||
- abseil/strings/cordz_update_tracker
|
|
||||||
- abseil/strings/internal
|
|
||||||
- abseil/strings/str_format
|
|
||||||
- abseil/strings/strings
|
|
||||||
- abseil/types/optional
|
|
||||||
- abseil/strings/cord_internal (1.20211102.0):
|
|
||||||
- abseil/base/base_internal
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/base/endian
|
|
||||||
- abseil/base/raw_logging_internal
|
|
||||||
- abseil/base/throw_delegate
|
|
||||||
- abseil/container/compressed_tuple
|
|
||||||
- abseil/container/inlined_vector
|
|
||||||
- abseil/container/layout
|
|
||||||
- abseil/functional/function_ref
|
|
||||||
- abseil/meta/type_traits
|
|
||||||
- abseil/strings/strings
|
|
||||||
- abseil/types/span
|
|
||||||
- abseil/strings/cordz_functions (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/base/raw_logging_internal
|
|
||||||
- abseil/profiling/exponential_biased
|
|
||||||
- abseil/strings/cordz_handle (1.20211102.0):
|
|
||||||
- abseil/base/base
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/raw_logging_internal
|
|
||||||
- abseil/synchronization/synchronization
|
|
||||||
- abseil/strings/cordz_info (1.20211102.0):
|
|
||||||
- abseil/base/base
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/base/raw_logging_internal
|
|
||||||
- abseil/container/inlined_vector
|
|
||||||
- abseil/debugging/stacktrace
|
|
||||||
- abseil/strings/cord_internal
|
|
||||||
- abseil/strings/cordz_functions
|
|
||||||
- abseil/strings/cordz_handle
|
|
||||||
- abseil/strings/cordz_statistics
|
|
||||||
- abseil/strings/cordz_update_tracker
|
|
||||||
- abseil/synchronization/synchronization
|
|
||||||
- abseil/types/span
|
|
||||||
- abseil/strings/cordz_statistics (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/strings/cordz_update_tracker
|
|
||||||
- abseil/strings/cordz_update_scope (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/strings/cord_internal
|
|
||||||
- abseil/strings/cordz_info
|
|
||||||
- abseil/strings/cordz_update_tracker
|
|
||||||
- abseil/strings/cordz_update_tracker (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/strings/internal (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/base/endian
|
|
||||||
- abseil/base/raw_logging_internal
|
|
||||||
- abseil/meta/type_traits
|
|
||||||
- abseil/strings/str_format (1.20211102.0):
|
|
||||||
- abseil/strings/str_format_internal
|
|
||||||
- abseil/strings/str_format_internal (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/functional/function_ref
|
|
||||||
- abseil/meta/type_traits
|
|
||||||
- abseil/numeric/bits
|
|
||||||
- abseil/numeric/int128
|
|
||||||
- abseil/numeric/representation
|
|
||||||
- abseil/strings/strings
|
|
||||||
- abseil/types/optional
|
|
||||||
- abseil/types/span
|
|
||||||
- abseil/strings/strings (1.20211102.0):
|
|
||||||
- abseil/base/base
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/base/endian
|
|
||||||
- abseil/base/raw_logging_internal
|
|
||||||
- abseil/base/throw_delegate
|
|
||||||
- abseil/memory/memory
|
|
||||||
- abseil/meta/type_traits
|
|
||||||
- abseil/numeric/bits
|
|
||||||
- abseil/numeric/int128
|
|
||||||
- abseil/strings/internal
|
|
||||||
- abseil/synchronization/graphcycles_internal (1.20211102.0):
|
|
||||||
- abseil/base/base
|
|
||||||
- abseil/base/base_internal
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/base/malloc_internal
|
|
||||||
- abseil/base/raw_logging_internal
|
|
||||||
- abseil/synchronization/kernel_timeout_internal (1.20211102.0):
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/base/raw_logging_internal
|
|
||||||
- abseil/time/time
|
|
||||||
- abseil/synchronization/synchronization (1.20211102.0):
|
|
||||||
- abseil/base/atomic_hook
|
|
||||||
- abseil/base/base
|
|
||||||
- abseil/base/base_internal
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/base/dynamic_annotations
|
|
||||||
- abseil/base/malloc_internal
|
|
||||||
- abseil/base/raw_logging_internal
|
|
||||||
- abseil/debugging/stacktrace
|
|
||||||
- abseil/debugging/symbolize
|
|
||||||
- abseil/synchronization/graphcycles_internal
|
|
||||||
- abseil/synchronization/kernel_timeout_internal
|
|
||||||
- abseil/time/time
|
|
||||||
- abseil/time (1.20211102.0):
|
|
||||||
- abseil/time/internal (= 1.20211102.0)
|
|
||||||
- abseil/time/time (= 1.20211102.0)
|
|
||||||
- abseil/time/internal (1.20211102.0):
|
|
||||||
- abseil/time/internal/cctz (= 1.20211102.0)
|
|
||||||
- abseil/time/internal/cctz (1.20211102.0):
|
|
||||||
- abseil/time/internal/cctz/civil_time (= 1.20211102.0)
|
|
||||||
- abseil/time/internal/cctz/time_zone (= 1.20211102.0)
|
|
||||||
- abseil/time/internal/cctz/civil_time (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/time/internal/cctz/time_zone (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/time/internal/cctz/civil_time
|
|
||||||
- abseil/time/time (1.20211102.0):
|
|
||||||
- abseil/base/base
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/base/raw_logging_internal
|
|
||||||
- abseil/numeric/int128
|
|
||||||
- abseil/strings/strings
|
|
||||||
- abseil/time/internal/cctz/civil_time
|
|
||||||
- abseil/time/internal/cctz/time_zone
|
|
||||||
- abseil/types (1.20211102.0):
|
|
||||||
- abseil/types/any (= 1.20211102.0)
|
|
||||||
- abseil/types/bad_any_cast (= 1.20211102.0)
|
|
||||||
- abseil/types/bad_any_cast_impl (= 1.20211102.0)
|
|
||||||
- abseil/types/bad_optional_access (= 1.20211102.0)
|
|
||||||
- abseil/types/bad_variant_access (= 1.20211102.0)
|
|
||||||
- abseil/types/compare (= 1.20211102.0)
|
|
||||||
- abseil/types/optional (= 1.20211102.0)
|
|
||||||
- abseil/types/span (= 1.20211102.0)
|
|
||||||
- abseil/types/variant (= 1.20211102.0)
|
|
||||||
- abseil/types/any (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/base/fast_type_id
|
|
||||||
- abseil/meta/type_traits
|
|
||||||
- abseil/types/bad_any_cast
|
|
||||||
- abseil/utility/utility
|
|
||||||
- abseil/types/bad_any_cast (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/types/bad_any_cast_impl
|
|
||||||
- abseil/types/bad_any_cast_impl (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/raw_logging_internal
|
|
||||||
- abseil/types/bad_optional_access (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/raw_logging_internal
|
|
||||||
- abseil/types/bad_variant_access (1.20211102.0):
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/raw_logging_internal
|
|
||||||
- abseil/types/compare (1.20211102.0):
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/meta/type_traits
|
|
||||||
- abseil/types/optional (1.20211102.0):
|
|
||||||
- abseil/base/base_internal
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/memory/memory
|
|
||||||
- abseil/meta/type_traits
|
|
||||||
- abseil/types/bad_optional_access
|
|
||||||
- abseil/utility/utility
|
|
||||||
- abseil/types/span (1.20211102.0):
|
|
||||||
- abseil/algorithm/algorithm
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/base/throw_delegate
|
|
||||||
- abseil/meta/type_traits
|
|
||||||
- abseil/types/variant (1.20211102.0):
|
|
||||||
- abseil/base/base_internal
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/base/core_headers
|
|
||||||
- abseil/meta/type_traits
|
|
||||||
- abseil/types/bad_variant_access
|
|
||||||
- abseil/utility/utility
|
|
||||||
- abseil/utility/utility (1.20211102.0):
|
|
||||||
- abseil/base/base_internal
|
|
||||||
- abseil/base/config
|
|
||||||
- abseil/meta/type_traits
|
|
||||||
- BoringSSL-GRPC (0.0.24):
|
|
||||||
- BoringSSL-GRPC/Implementation (= 0.0.24)
|
|
||||||
- BoringSSL-GRPC/Interface (= 0.0.24)
|
|
||||||
- BoringSSL-GRPC/Implementation (0.0.24):
|
|
||||||
- BoringSSL-GRPC/Interface (= 0.0.24)
|
|
||||||
- BoringSSL-GRPC/Interface (0.0.24)
|
|
||||||
- cloud_firestore (4.6.0):
|
|
||||||
- Firebase/Firestore (= 10.7.0)
|
|
||||||
- firebase_core
|
|
||||||
- Flutter
|
|
||||||
- nanopb (< 2.30910.0, >= 2.30908.0)
|
|
||||||
- Firebase/Auth (10.7.0):
|
|
||||||
- Firebase/CoreOnly
|
|
||||||
- FirebaseAuth (~> 10.7.0)
|
|
||||||
- Firebase/CoreOnly (10.7.0):
|
|
||||||
- FirebaseCore (= 10.7.0)
|
|
||||||
- Firebase/Firestore (10.7.0):
|
|
||||||
- Firebase/CoreOnly
|
|
||||||
- FirebaseFirestore (~> 10.7.0)
|
|
||||||
- Firebase/Storage (10.7.0):
|
|
||||||
- Firebase/CoreOnly
|
|
||||||
- FirebaseStorage (~> 10.7.0)
|
|
||||||
- firebase_auth (4.5.0):
|
|
||||||
- Firebase/Auth (= 10.7.0)
|
|
||||||
- firebase_core
|
|
||||||
- Flutter
|
|
||||||
- firebase_core (2.11.0):
|
|
||||||
- Firebase/CoreOnly (= 10.7.0)
|
|
||||||
- Flutter
|
|
||||||
- firebase_storage (11.1.2):
|
|
||||||
- Firebase/Storage (= 10.7.0)
|
|
||||||
- firebase_core
|
|
||||||
- Flutter
|
|
||||||
- FirebaseAppCheckInterop (10.9.0)
|
|
||||||
- FirebaseAuth (10.7.0):
|
|
||||||
- FirebaseCore (~> 10.0)
|
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 7.8)
|
|
||||||
- GoogleUtilities/Environment (~> 7.8)
|
|
||||||
- GTMSessionFetcher/Core (< 4.0, >= 2.1)
|
|
||||||
- FirebaseAuthInterop (10.9.0)
|
|
||||||
- FirebaseCore (10.7.0):
|
|
||||||
- FirebaseCoreInternal (~> 10.0)
|
|
||||||
- GoogleUtilities/Environment (~> 7.8)
|
|
||||||
- GoogleUtilities/Logger (~> 7.8)
|
|
||||||
- FirebaseCoreExtension (10.9.0):
|
|
||||||
- FirebaseCore (~> 10.0)
|
|
||||||
- FirebaseCoreInternal (10.9.0):
|
|
||||||
- "GoogleUtilities/NSData+zlib (~> 7.8)"
|
|
||||||
- FirebaseFirestore (10.7.0):
|
|
||||||
- abseil/algorithm (~> 1.20211102.0)
|
|
||||||
- abseil/base (~> 1.20211102.0)
|
|
||||||
- abseil/container/flat_hash_map (~> 1.20211102.0)
|
|
||||||
- abseil/memory (~> 1.20211102.0)
|
|
||||||
- abseil/meta (~> 1.20211102.0)
|
|
||||||
- abseil/strings/strings (~> 1.20211102.0)
|
|
||||||
- abseil/time (~> 1.20211102.0)
|
|
||||||
- abseil/types (~> 1.20211102.0)
|
|
||||||
- FirebaseCore (~> 10.0)
|
|
||||||
- "gRPC-C++ (~> 1.44.0)"
|
|
||||||
- leveldb-library (~> 1.22)
|
|
||||||
- nanopb (< 2.30910.0, >= 2.30908.0)
|
|
||||||
- FirebaseStorage (10.7.0):
|
|
||||||
- FirebaseAppCheckInterop (~> 10.0)
|
|
||||||
- FirebaseAuthInterop (~> 10.0)
|
|
||||||
- FirebaseCore (~> 10.0)
|
|
||||||
- FirebaseCoreExtension (~> 10.0)
|
|
||||||
- GTMSessionFetcher/Core (< 4.0, >= 2.1)
|
|
||||||
- Flutter (1.0.0)
|
|
||||||
- FMDB (2.7.5):
|
|
||||||
- FMDB/standard (= 2.7.5)
|
|
||||||
- FMDB/standard (2.7.5)
|
|
||||||
- GoogleUtilities/AppDelegateSwizzler (7.11.1):
|
|
||||||
- GoogleUtilities/Environment
|
|
||||||
- GoogleUtilities/Logger
|
|
||||||
- GoogleUtilities/Network
|
|
||||||
- GoogleUtilities/Environment (7.11.1):
|
|
||||||
- PromisesObjC (< 3.0, >= 1.2)
|
|
||||||
- GoogleUtilities/Logger (7.11.1):
|
|
||||||
- GoogleUtilities/Environment
|
|
||||||
- GoogleUtilities/Network (7.11.1):
|
|
||||||
- GoogleUtilities/Logger
|
|
||||||
- "GoogleUtilities/NSData+zlib"
|
|
||||||
- GoogleUtilities/Reachability
|
|
||||||
- "GoogleUtilities/NSData+zlib (7.11.1)"
|
|
||||||
- GoogleUtilities/Reachability (7.11.1):
|
|
||||||
- GoogleUtilities/Logger
|
|
||||||
- "gRPC-C++ (1.44.0)":
|
|
||||||
- "gRPC-C++/Implementation (= 1.44.0)"
|
|
||||||
- "gRPC-C++/Interface (= 1.44.0)"
|
|
||||||
- "gRPC-C++/Implementation (1.44.0)":
|
|
||||||
- abseil/base/base (= 1.20211102.0)
|
|
||||||
- abseil/base/core_headers (= 1.20211102.0)
|
|
||||||
- abseil/container/flat_hash_map (= 1.20211102.0)
|
|
||||||
- abseil/container/inlined_vector (= 1.20211102.0)
|
|
||||||
- abseil/functional/bind_front (= 1.20211102.0)
|
|
||||||
- abseil/hash/hash (= 1.20211102.0)
|
|
||||||
- abseil/memory/memory (= 1.20211102.0)
|
|
||||||
- abseil/random/random (= 1.20211102.0)
|
|
||||||
- abseil/status/status (= 1.20211102.0)
|
|
||||||
- abseil/status/statusor (= 1.20211102.0)
|
|
||||||
- abseil/strings/cord (= 1.20211102.0)
|
|
||||||
- abseil/strings/str_format (= 1.20211102.0)
|
|
||||||
- abseil/strings/strings (= 1.20211102.0)
|
|
||||||
- abseil/synchronization/synchronization (= 1.20211102.0)
|
|
||||||
- abseil/time/time (= 1.20211102.0)
|
|
||||||
- abseil/types/optional (= 1.20211102.0)
|
|
||||||
- abseil/types/variant (= 1.20211102.0)
|
|
||||||
- abseil/utility/utility (= 1.20211102.0)
|
|
||||||
- "gRPC-C++/Interface (= 1.44.0)"
|
|
||||||
- gRPC-Core (= 1.44.0)
|
|
||||||
- "gRPC-C++/Interface (1.44.0)"
|
|
||||||
- gRPC-Core (1.44.0):
|
|
||||||
- gRPC-Core/Implementation (= 1.44.0)
|
|
||||||
- gRPC-Core/Interface (= 1.44.0)
|
|
||||||
- gRPC-Core/Implementation (1.44.0):
|
|
||||||
- abseil/base/base (= 1.20211102.0)
|
|
||||||
- abseil/base/core_headers (= 1.20211102.0)
|
|
||||||
- abseil/container/flat_hash_map (= 1.20211102.0)
|
|
||||||
- abseil/container/inlined_vector (= 1.20211102.0)
|
|
||||||
- abseil/functional/bind_front (= 1.20211102.0)
|
|
||||||
- abseil/hash/hash (= 1.20211102.0)
|
|
||||||
- abseil/memory/memory (= 1.20211102.0)
|
|
||||||
- abseil/random/random (= 1.20211102.0)
|
|
||||||
- abseil/status/status (= 1.20211102.0)
|
|
||||||
- abseil/status/statusor (= 1.20211102.0)
|
|
||||||
- abseil/strings/cord (= 1.20211102.0)
|
|
||||||
- abseil/strings/str_format (= 1.20211102.0)
|
|
||||||
- abseil/strings/strings (= 1.20211102.0)
|
|
||||||
- abseil/synchronization/synchronization (= 1.20211102.0)
|
|
||||||
- abseil/time/time (= 1.20211102.0)
|
|
||||||
- abseil/types/optional (= 1.20211102.0)
|
|
||||||
- abseil/types/variant (= 1.20211102.0)
|
|
||||||
- abseil/utility/utility (= 1.20211102.0)
|
|
||||||
- BoringSSL-GRPC (= 0.0.24)
|
|
||||||
- gRPC-Core/Interface (= 1.44.0)
|
|
||||||
- Libuv-gRPC (= 0.0.10)
|
|
||||||
- gRPC-Core/Interface (1.44.0)
|
|
||||||
- GTMSessionFetcher/Core (3.1.1)
|
|
||||||
- image_picker_ios (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- leveldb-library (1.22.2)
|
|
||||||
- Libuv-gRPC (0.0.10):
|
|
||||||
- Libuv-gRPC/Implementation (= 0.0.10)
|
|
||||||
- Libuv-gRPC/Interface (= 0.0.10)
|
|
||||||
- Libuv-gRPC/Implementation (0.0.10):
|
|
||||||
- Libuv-gRPC/Interface (= 0.0.10)
|
|
||||||
- Libuv-gRPC/Interface (0.0.10)
|
|
||||||
- nanopb (2.30909.0):
|
|
||||||
- nanopb/decode (= 2.30909.0)
|
|
||||||
- nanopb/encode (= 2.30909.0)
|
|
||||||
- nanopb/decode (2.30909.0)
|
|
||||||
- nanopb/encode (2.30909.0)
|
|
||||||
- path_provider_foundation (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
- PromisesObjC (2.2.0)
|
|
||||||
- sqflite (0.0.2):
|
|
||||||
- Flutter
|
|
||||||
- FMDB (>= 2.7.5)
|
|
||||||
|
|
||||||
DEPENDENCIES:
|
|
||||||
- cloud_firestore (from `.symlinks/plugins/cloud_firestore/ios`)
|
|
||||||
- firebase_auth (from `.symlinks/plugins/firebase_auth/ios`)
|
|
||||||
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
|
|
||||||
- firebase_storage (from `.symlinks/plugins/firebase_storage/ios`)
|
|
||||||
- Flutter (from `Flutter`)
|
|
||||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
|
||||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`)
|
|
||||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
|
||||||
|
|
||||||
SPEC REPOS:
|
|
||||||
trunk:
|
|
||||||
- abseil
|
|
||||||
- BoringSSL-GRPC
|
|
||||||
- Firebase
|
|
||||||
- FirebaseAppCheckInterop
|
|
||||||
- FirebaseAuth
|
|
||||||
- FirebaseAuthInterop
|
|
||||||
- FirebaseCore
|
|
||||||
- FirebaseCoreExtension
|
|
||||||
- FirebaseCoreInternal
|
|
||||||
- FirebaseFirestore
|
|
||||||
- FirebaseStorage
|
|
||||||
- FMDB
|
|
||||||
- GoogleUtilities
|
|
||||||
- "gRPC-C++"
|
|
||||||
- gRPC-Core
|
|
||||||
- GTMSessionFetcher
|
|
||||||
- leveldb-library
|
|
||||||
- Libuv-gRPC
|
|
||||||
- nanopb
|
|
||||||
- PromisesObjC
|
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
|
||||||
cloud_firestore:
|
|
||||||
:path: ".symlinks/plugins/cloud_firestore/ios"
|
|
||||||
firebase_auth:
|
|
||||||
:path: ".symlinks/plugins/firebase_auth/ios"
|
|
||||||
firebase_core:
|
|
||||||
:path: ".symlinks/plugins/firebase_core/ios"
|
|
||||||
firebase_storage:
|
|
||||||
:path: ".symlinks/plugins/firebase_storage/ios"
|
|
||||||
Flutter:
|
|
||||||
:path: Flutter
|
|
||||||
image_picker_ios:
|
|
||||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
|
||||||
path_provider_foundation:
|
|
||||||
:path: ".symlinks/plugins/path_provider_foundation/ios"
|
|
||||||
sqflite:
|
|
||||||
:path: ".symlinks/plugins/sqflite/ios"
|
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
|
||||||
abseil: ebe5b5529fb05d93a8bdb7951607be08b7fa71bc
|
|
||||||
BoringSSL-GRPC: 3175b25143e648463a56daeaaa499c6cb86dad33
|
|
||||||
cloud_firestore: 3809dfc121ab99b81bba51eb84ac6ca3f9033325
|
|
||||||
Firebase: 0219acf760880eeec8ce479895bd7767466d9f81
|
|
||||||
firebase_auth: 1eb69de8f57040c5c78bb3267412e9cb395fd08c
|
|
||||||
firebase_core: dee76ded6c693fdb38b8ea39aef7129e32e587a3
|
|
||||||
firebase_storage: f6fb3e54e886182b5ab5cf514da1d9ceb196fa2f
|
|
||||||
FirebaseAppCheckInterop: e69dde5cd51b88ee1b4339d6766b691272256f9b
|
|
||||||
FirebaseAuth: dd64c01631df724b09f33e584625775c52f7d71f
|
|
||||||
FirebaseAuthInterop: e53c08e60a02de17d1ab77c5032db8ae22d3a799
|
|
||||||
FirebaseCore: e317665b9d744727a97e623edbbed009320afdd7
|
|
||||||
FirebaseCoreExtension: d3e9bba2930a8033042112397cd9f006a1bb203d
|
|
||||||
FirebaseCoreInternal: d2b4acb827908e72eca47a9fd896767c3053921e
|
|
||||||
FirebaseFirestore: 3963a6edd1c84b4748dab3e2c62624a29d03eca1
|
|
||||||
FirebaseStorage: 4841efa304543e1f9e4ca116c559c7a1ea2a9d0f
|
|
||||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
|
||||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
|
||||||
GoogleUtilities: 9aa0ad5a7bc171f8bae016300bfcfa3fb8425749
|
|
||||||
"gRPC-C++": 9675f953ace2b3de7c506039d77be1f2e77a8db2
|
|
||||||
gRPC-Core: 943e491cb0d45598b0b0eb9e910c88080369290b
|
|
||||||
GTMSessionFetcher: e8647203b65cee28c5f73d0f473d096653945e72
|
|
||||||
image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5
|
|
||||||
leveldb-library: f03246171cce0484482ec291f88b6d563699ee06
|
|
||||||
Libuv-gRPC: 55e51798e14ef436ad9bc45d12d43b77b49df378
|
|
||||||
nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431
|
|
||||||
path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9
|
|
||||||
PromisesObjC: 09985d6d70fbe7878040aa746d78236e6946d2ef
|
|
||||||
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
|
||||||
|
|
||||||
PODFILE CHECKSUM: 7d6b9db9243dbc65e9914f56de22b83205a8e90d
|
|
||||||
|
|
||||||
COCOAPODS: 1.12.1
|
|
||||||
@@ -160,7 +160,7 @@
|
|||||||
97C146E61CF9000F007C117D /* Project object */ = {
|
97C146E61CF9000F007C117D /* Project object */ = {
|
||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
LastUpgradeCheck = 1300;
|
LastUpgradeCheck = 1510;
|
||||||
ORGANIZATIONNAME = "";
|
ORGANIZATIONNAME = "";
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
97C146ED1CF9000F007C117D = {
|
97C146ED1CF9000F007C117D = {
|
||||||
@@ -348,7 +348,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SUPPORTED_PLATFORMS = iphoneos;
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
@@ -363,13 +363,13 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = 4YF7BFHHRH;
|
DEVELOPMENT_TEAM = 4YF7BFHHRH;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@@ -436,7 +436,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -486,7 +486,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SUPPORTED_PLATFORMS = iphoneos;
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
@@ -503,13 +503,13 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = 4YF7BFHHRH;
|
DEVELOPMENT_TEAM = 4YF7BFHHRH;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@@ -535,13 +535,13 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = 4YF7BFHHRH;
|
DEVELOPMENT_TEAM = 4YF7BFHHRH;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import UIKit
|
|
||||||
import Flutter
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
|
||||||
@UIApplicationMain
|
@main
|
||||||
@objc class AppDelegate: FlutterAppDelegate {
|
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||||
override func application(
|
override func application(
|
||||||
_ application: UIApplication,
|
_ application: UIApplication,
|
||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
GeneratedPluginRegistrant.register(with: self)
|
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||||
|
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 68 B |
Binary file not shown.
|
After Width: | Height: | Size: 68 B |
Binary file not shown.
|
After Width: | Height: | Size: 68 B |
@@ -0,0 +1,5 @@
|
|||||||
|
# Launch Screen Assets
|
||||||
|
|
||||||
|
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
|
||||||
|
|
||||||
|
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
|
||||||
@@ -26,6 +26,27 @@
|
|||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>UIApplicationSceneManifest</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIApplicationSupportsMultipleScenes</key>
|
||||||
|
<false/>
|
||||||
|
<key>UISceneConfigurations</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIWindowSceneSessionRoleApplication</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>UISceneClassName</key>
|
||||||
|
<string>UIWindowScene</string>
|
||||||
|
<key>UISceneConfigurationName</key>
|
||||||
|
<string>flutter</string>
|
||||||
|
<key>UISceneDelegateClassName</key>
|
||||||
|
<string>FlutterSceneDelegate</string>
|
||||||
|
<key>UISceneStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
|
|||||||
+1
-1
@@ -38,7 +38,7 @@ class MyApp extends StatelessWidget {
|
|||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
foregroundColor: Colors.black87,
|
foregroundColor: Colors.black87,
|
||||||
),
|
),
|
||||||
cardTheme: CardTheme(
|
cardTheme: CardThemeData(
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
|||||||
@@ -19,17 +19,20 @@ class PostItem {
|
|||||||
required this.id,
|
required this.id,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.description,
|
required this.description,
|
||||||
this.imageUrl = '',
|
String imageUrl = '',
|
||||||
List<String>? imageUrls,
|
List<String>? imageUrls,
|
||||||
required this.createdDate,
|
required this.createdDate,
|
||||||
this.authorUid,
|
this.authorUid,
|
||||||
this.street,
|
this.street,
|
||||||
this.category,
|
this.category,
|
||||||
this.status = 'Gemeldet',
|
this.status,
|
||||||
this.pdfUrl,
|
this.pdfUrl,
|
||||||
this.webUrl,
|
this.webUrl,
|
||||||
this.isAussendung = false,
|
this.isAussendung = false,
|
||||||
}) : imageUrls = imageUrls ?? (imageUrl.isNotEmpty ? [imageUrl] : []);
|
}) : imageUrls = imageUrls ?? (imageUrl.isNotEmpty ? [imageUrl] : []),
|
||||||
|
imageUrl = imageUrl.isNotEmpty
|
||||||
|
? imageUrl
|
||||||
|
: (imageUrls != null && imageUrls.isNotEmpty ? imageUrls.first : '');
|
||||||
|
|
||||||
factory PostItem.fromFirestore(DocumentSnapshot doc) {
|
factory PostItem.fromFirestore(DocumentSnapshot doc) {
|
||||||
final data = doc.data() as Map<String, dynamic>? ?? {};
|
final data = doc.data() as Map<String, dynamic>? ?? {};
|
||||||
@@ -49,6 +52,9 @@ class PostItem {
|
|||||||
images = [legacyImage];
|
images = [legacyImage];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final hasIssueFields = data['street'] != null || data['category'] != null;
|
||||||
|
final status = data['status'] as String? ?? (hasIssueFields ? 'Gemeldet' : null);
|
||||||
|
|
||||||
return PostItem(
|
return PostItem(
|
||||||
id: doc.id,
|
id: doc.id,
|
||||||
title: data['title'] as String? ?? '',
|
title: data['title'] as String? ?? '',
|
||||||
@@ -59,7 +65,7 @@ class PostItem {
|
|||||||
authorUid: data['author_uid'] as String?,
|
authorUid: data['author_uid'] as String?,
|
||||||
street: data['street'] as String?,
|
street: data['street'] as String?,
|
||||||
category: data['category'] as String?,
|
category: data['category'] as String?,
|
||||||
status: data['status'] as String? ?? 'Gemeldet',
|
status: status,
|
||||||
pdfUrl: data['pdfUrl'] as String?,
|
pdfUrl: data['pdfUrl'] as String?,
|
||||||
webUrl: data['webUrl'] as String?,
|
webUrl: data['webUrl'] as String?,
|
||||||
isAussendung: data['isAussendung'] as bool? ?? false,
|
isAussendung: data['isAussendung'] as bool? ?? false,
|
||||||
@@ -119,17 +125,23 @@ class PostItem {
|
|||||||
images = [legacyImage];
|
images = [legacyImage];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final hasIssueFields = map['street'] != null || map['category'] != null;
|
||||||
|
final status =
|
||||||
|
map['status'] as String? ?? (hasIssueFields ? 'Gemeldet' : null);
|
||||||
|
|
||||||
return PostItem(
|
return PostItem(
|
||||||
id: map['id'] as String? ?? '',
|
id: map['id'] as String? ?? '',
|
||||||
title: map['title'] as String? ?? '',
|
title: map['title'] as String? ?? '',
|
||||||
description: map['description'] as String? ?? '',
|
description: map['description'] as String? ?? '',
|
||||||
imageUrl: legacyImage.isNotEmpty ? legacyImage : (images.isNotEmpty ? images.first : ''),
|
imageUrl: legacyImage.isNotEmpty
|
||||||
|
? legacyImage
|
||||||
|
: (images.isNotEmpty ? images.first : ''),
|
||||||
imageUrls: images,
|
imageUrls: images,
|
||||||
createdDate: date,
|
createdDate: date,
|
||||||
authorUid: map['authorUid'] as String?,
|
authorUid: map['authorUid'] as String?,
|
||||||
street: map['street'] as String?,
|
street: map['street'] as String?,
|
||||||
category: map['category'] as String?,
|
category: map['category'] as String?,
|
||||||
status: map['status'] as String? ?? 'Gemeldet',
|
status: status,
|
||||||
pdfUrl: map['pdfUrl'] as String?,
|
pdfUrl: map['pdfUrl'] as String?,
|
||||||
webUrl: map['webUrl'] as String?,
|
webUrl: map['webUrl'] as String?,
|
||||||
isAussendung: map['isAussendung'] as bool? ?? false,
|
isAussendung: map['isAussendung'] as bool? ?? false,
|
||||||
@@ -236,4 +248,37 @@ class PostItem {
|
|||||||
isAussendung: true,
|
isAussendung: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a copy of this PostItem with updated fields
|
||||||
|
PostItem copyWith({
|
||||||
|
String? id,
|
||||||
|
String? title,
|
||||||
|
String? description,
|
||||||
|
String? imageUrl,
|
||||||
|
List<String>? imageUrls,
|
||||||
|
DateTime? createdDate,
|
||||||
|
String? authorUid,
|
||||||
|
String? street,
|
||||||
|
String? category,
|
||||||
|
String? status,
|
||||||
|
String? pdfUrl,
|
||||||
|
String? webUrl,
|
||||||
|
bool? isAussendung,
|
||||||
|
}) {
|
||||||
|
return PostItem(
|
||||||
|
id: id ?? this.id,
|
||||||
|
title: title ?? this.title,
|
||||||
|
description: description ?? this.description,
|
||||||
|
imageUrl: imageUrl ?? this.imageUrl,
|
||||||
|
imageUrls: imageUrls ?? this.imageUrls,
|
||||||
|
createdDate: createdDate ?? this.createdDate,
|
||||||
|
authorUid: authorUid ?? this.authorUid,
|
||||||
|
street: street ?? this.street,
|
||||||
|
category: category ?? this.category,
|
||||||
|
status: status ?? this.status,
|
||||||
|
pdfUrl: pdfUrl ?? this.pdfUrl,
|
||||||
|
webUrl: webUrl ?? this.webUrl,
|
||||||
|
isAussendung: isAussendung ?? this.isAussendung,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:firebase_auth/firebase_auth.dart';
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import '../models/post_item.dart';
|
import '../models/post_item.dart';
|
||||||
|
import '../services/auth_service.dart';
|
||||||
import '../services/firestore_service.dart';
|
import '../services/firestore_service.dart';
|
||||||
import '../services/storage_service.dart';
|
import '../services/storage_service.dart';
|
||||||
|
|
||||||
class AddIssueScreen extends StatefulWidget {
|
class AddIssueScreen extends StatefulWidget {
|
||||||
const AddIssueScreen({super.key});
|
final PostItem? issueToEdit;
|
||||||
|
|
||||||
|
const AddIssueScreen({super.key, this.issueToEdit});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AddIssueScreen> createState() => _AddIssueScreenState();
|
State<AddIssueScreen> createState() => _AddIssueScreenState();
|
||||||
@@ -22,8 +27,18 @@ class _AddIssueScreenState extends State<AddIssueScreen> {
|
|||||||
final StorageService _storageService = StorageService();
|
final StorageService _storageService = StorageService();
|
||||||
|
|
||||||
final List<File> _selectedImages = [];
|
final List<File> _selectedImages = [];
|
||||||
|
List<String> _existingImageUrls = [];
|
||||||
|
String _selectedStatus = 'Gemeldet';
|
||||||
bool _isSaving = false;
|
bool _isSaving = false;
|
||||||
|
|
||||||
|
bool get isEditing => widget.issueToEdit != null;
|
||||||
|
|
||||||
|
final List<String> _statuses = [
|
||||||
|
'Gemeldet',
|
||||||
|
'In Bearbeitung',
|
||||||
|
'Erledigt',
|
||||||
|
];
|
||||||
|
|
||||||
final List<String> _streets = [
|
final List<String> _streets = [
|
||||||
'Hauptstraße',
|
'Hauptstraße',
|
||||||
'Seestraße',
|
'Seestraße',
|
||||||
@@ -56,9 +71,32 @@ class _AddIssueScreenState extends State<AddIssueScreen> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
if (widget.issueToEdit != null) {
|
||||||
|
final issue = widget.issueToEdit!;
|
||||||
|
_titleController.text = issue.title;
|
||||||
|
_descriptionController.text = issue.description;
|
||||||
|
if (issue.street != null && _streets.contains(issue.street)) {
|
||||||
|
_selectedStreet = issue.street;
|
||||||
|
} else {
|
||||||
|
_selectedStreet = _streets.first;
|
||||||
|
}
|
||||||
|
if (issue.category != null && _categories.contains(issue.category)) {
|
||||||
|
_selectedCategory = issue.category;
|
||||||
|
} else {
|
||||||
|
_selectedCategory = _categories.first;
|
||||||
|
}
|
||||||
|
_existingImageUrls = List<String>.from(issue.imageUrls);
|
||||||
|
if (_existingImageUrls.isEmpty && issue.imageUrl.isNotEmpty) {
|
||||||
|
_existingImageUrls.add(issue.imageUrl);
|
||||||
|
}
|
||||||
|
if (issue.status != null && _statuses.contains(issue.status)) {
|
||||||
|
_selectedStatus = issue.status!;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
_selectedStreet = _streets.first;
|
_selectedStreet = _streets.first;
|
||||||
_selectedCategory = _categories.first;
|
_selectedCategory = _categories.first;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@@ -68,9 +106,10 @@ class _AddIssueScreenState extends State<AddIssueScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _pickImage(ImageSource source) async {
|
Future<void> _pickImage(ImageSource source) async {
|
||||||
if (_selectedImages.length >= 2) {
|
if (_existingImageUrls.length + _selectedImages.length >= 2) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Es können maximal 2 Fotos hinzugefügt werden.')),
|
const SnackBar(
|
||||||
|
content: Text('Es können maximal 2 Fotos hinzugefügt werden.')),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -99,6 +138,7 @@ class _AddIssueScreenState extends State<AddIssueScreen> {
|
|||||||
void _showImageSourceModal() {
|
void _showImageSourceModal() {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
|
useSafeArea: true,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||||
),
|
),
|
||||||
@@ -135,6 +175,51 @@ class _AddIssueScreenState extends State<AddIssueScreen> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (isEditing) {
|
||||||
|
// Delete removed old images from Storage
|
||||||
|
for (final oldUrl in widget.issueToEdit!.imageUrls) {
|
||||||
|
if (!_existingImageUrls.contains(oldUrl)) {
|
||||||
|
try {
|
||||||
|
await _storageService.deleteFileByUrl(oldUrl);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload newly selected images to Storage
|
||||||
|
final List<String> newlyUploadedUrls = [];
|
||||||
|
for (final file in _selectedImages) {
|
||||||
|
final url = await _storageService.uploadFile(
|
||||||
|
file: file,
|
||||||
|
collectionName: 'Issues',
|
||||||
|
optimize: true,
|
||||||
|
);
|
||||||
|
newlyUploadedUrls.add(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
final allImages = [..._existingImageUrls, ...newlyUploadedUrls];
|
||||||
|
|
||||||
|
final updatedIssue = widget.issueToEdit!.copyWith(
|
||||||
|
title: _titleController.text.trim(),
|
||||||
|
description: _descriptionController.text.trim(),
|
||||||
|
imageUrl: allImages.isNotEmpty ? allImages.first : '',
|
||||||
|
imageUrls: allImages,
|
||||||
|
street: _selectedStreet,
|
||||||
|
category: _selectedCategory,
|
||||||
|
status: _selectedStatus,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _firestoreService.updatePost('Issues', updatedIssue);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Mangel erfolgreich aktualisiert!'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Navigator.pop(context, updatedIssue);
|
||||||
|
} else {
|
||||||
final currentUserId = FirebaseAuth.instance.currentUser?.uid;
|
final currentUserId = FirebaseAuth.instance.currentUser?.uid;
|
||||||
|
|
||||||
// Upload selected images to Firebase Storage
|
// Upload selected images to Firebase Storage
|
||||||
@@ -171,7 +256,8 @@ class _AddIssueScreenState extends State<AddIssueScreen> {
|
|||||||
backgroundColor: Colors.green,
|
backgroundColor: Colors.green,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context, newIssue);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@@ -191,12 +277,21 @@ class _AddIssueScreenState extends State<AddIssueScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final auth = Provider.of<AuthService>(context, listen: false);
|
||||||
|
final isAdmin = auth.isAdmin;
|
||||||
|
final totalPhotos = _existingImageUrls.length + _selectedImages.length;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Mangel melden'),
|
title: Text(isEditing ? 'Mangel bearbeiten' : 'Mangel melden'),
|
||||||
),
|
),
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: EdgeInsets.only(
|
||||||
|
left: 16.0,
|
||||||
|
right: 16.0,
|
||||||
|
top: 16.0,
|
||||||
|
bottom: MediaQuery.paddingOf(context).bottom + 24.0,
|
||||||
|
),
|
||||||
child: Form(
|
child: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -215,7 +310,7 @@ class _AddIssueScreenState extends State<AddIssueScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
value: _selectedStreet,
|
initialValue: _selectedStreet,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Straße / Örtlichkeit *',
|
labelText: 'Straße / Örtlichkeit *',
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
@@ -232,7 +327,7 @@ class _AddIssueScreenState extends State<AddIssueScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
value: _selectedCategory,
|
initialValue: _selectedCategory,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Grundlage / Kategorie *',
|
labelText: 'Grundlage / Kategorie *',
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
@@ -245,6 +340,26 @@ class _AddIssueScreenState extends State<AddIssueScreen> {
|
|||||||
.toList(),
|
.toList(),
|
||||||
onChanged: (val) => setState(() => _selectedCategory = val),
|
onChanged: (val) => setState(() => _selectedCategory = val),
|
||||||
),
|
),
|
||||||
|
if (isAdmin && isEditing) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
DropdownButtonFormField<String>(
|
||||||
|
initialValue: _selectedStatus,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Bearbeitungsstatus (Admin) *',
|
||||||
|
prefixIcon: Icon(Icons.assignment_turned_in_outlined),
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
items: _statuses
|
||||||
|
.map((status) => DropdownMenuItem(
|
||||||
|
value: status,
|
||||||
|
child: Text(status),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
onChanged: (val) {
|
||||||
|
if (val != null) setState(() => _selectedStatus = val);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _descriptionController,
|
controller: _descriptionController,
|
||||||
@@ -265,8 +380,60 @@ class _AddIssueScreenState extends State<AddIssueScreen> {
|
|||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Row(
|
SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
// Existing images from Firebase Storage
|
||||||
|
..._existingImageUrls.asMap().entries.map((entry) {
|
||||||
|
final index = entry.key;
|
||||||
|
final url = entry.value;
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.only(right: 12),
|
||||||
|
width: 110,
|
||||||
|
height: 80,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: Colors.grey.shade400),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: url,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
placeholder: (ctx, u) => const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
errorWidget: (ctx, u, e) => Container(
|
||||||
|
color: Colors.grey.shade200,
|
||||||
|
child: const Icon(Icons.broken_image, size: 28),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: -4,
|
||||||
|
right: 4,
|
||||||
|
child: CircleAvatar(
|
||||||
|
radius: 14,
|
||||||
|
backgroundColor: Colors.black54,
|
||||||
|
child: IconButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
icon: const Icon(Icons.close,
|
||||||
|
color: Colors.white, size: 16),
|
||||||
|
tooltip: 'Foto entfernen',
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_existingImageUrls.removeAt(index);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
// Newly picked local image files
|
||||||
..._selectedImages.asMap().entries.map((entry) {
|
..._selectedImages.asMap().entries.map((entry) {
|
||||||
final index = entry.key;
|
final index = entry.key;
|
||||||
final file = entry.value;
|
final file = entry.value;
|
||||||
@@ -288,8 +455,14 @@ class _AddIssueScreenState extends State<AddIssueScreen> {
|
|||||||
Positioned(
|
Positioned(
|
||||||
top: -4,
|
top: -4,
|
||||||
right: 4,
|
right: 4,
|
||||||
|
child: CircleAvatar(
|
||||||
|
radius: 14,
|
||||||
|
backgroundColor: Colors.black54,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: const Icon(Icons.cancel, color: Colors.red),
|
padding: EdgeInsets.zero,
|
||||||
|
icon: const Icon(Icons.close,
|
||||||
|
color: Colors.white, size: 16),
|
||||||
|
tooltip: 'Foto entfernen',
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedImages.removeAt(index);
|
_selectedImages.removeAt(index);
|
||||||
@@ -297,10 +470,11 @@ class _AddIssueScreenState extends State<AddIssueScreen> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
if (_selectedImages.length < 2)
|
if (totalPhotos < 2)
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: _showImageSourceModal,
|
onTap: _showImageSourceModal,
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -327,6 +501,7 @@ class _AddIssueScreenState extends State<AddIssueScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
@@ -340,9 +515,13 @@ class _AddIssueScreenState extends State<AddIssueScreen> {
|
|||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
strokeWidth: 2, color: Colors.white),
|
strokeWidth: 2, color: Colors.white),
|
||||||
)
|
)
|
||||||
: const Icon(Icons.check_circle_outline),
|
: Icon(isEditing ? Icons.save : Icons.check_circle_outline),
|
||||||
label: Text(
|
label: Text(
|
||||||
_isSaving ? 'Wird gespeichert...' : 'Mangel fertigstellen',
|
_isSaving
|
||||||
|
? 'Wird gespeichert...'
|
||||||
|
: (isEditing
|
||||||
|
? 'Änderungen speichern'
|
||||||
|
: 'Mangel fertigstellen'),
|
||||||
style: const TextStyle(fontSize: 16),
|
style: const TextStyle(fontSize: 16),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:firebase_auth/firebase_auth.dart';
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
@@ -9,11 +10,13 @@ import '../services/storage_service.dart';
|
|||||||
class AddPostScreen extends StatefulWidget {
|
class AddPostScreen extends StatefulWidget {
|
||||||
final String collectionName;
|
final String collectionName;
|
||||||
final String screenTitle;
|
final String screenTitle;
|
||||||
|
final PostItem? postToEdit;
|
||||||
|
|
||||||
const AddPostScreen({
|
const AddPostScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.collectionName,
|
required this.collectionName,
|
||||||
required this.screenTitle,
|
required this.screenTitle,
|
||||||
|
this.postToEdit,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -29,8 +32,25 @@ class _AddPostScreenState extends State<AddPostScreen> {
|
|||||||
final StorageService _storageService = StorageService();
|
final StorageService _storageService = StorageService();
|
||||||
|
|
||||||
File? _selectedImage;
|
File? _selectedImage;
|
||||||
|
String? _existingImageUrl;
|
||||||
bool _isSaving = false;
|
bool _isSaving = false;
|
||||||
|
|
||||||
|
bool get isEditing => widget.postToEdit != null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (widget.postToEdit != null) {
|
||||||
|
_titleController.text = widget.postToEdit!.title;
|
||||||
|
_descriptionController.text = widget.postToEdit!.description;
|
||||||
|
if (widget.postToEdit!.imageUrls.isNotEmpty) {
|
||||||
|
_existingImageUrl = widget.postToEdit!.imageUrls.first;
|
||||||
|
} else if (widget.postToEdit!.imageUrl.isNotEmpty) {
|
||||||
|
_existingImageUrl = widget.postToEdit!.imageUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_titleController.dispose();
|
_titleController.dispose();
|
||||||
@@ -63,6 +83,7 @@ class _AddPostScreenState extends State<AddPostScreen> {
|
|||||||
void _showImageSourceModal() {
|
void _showImageSourceModal() {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
|
useSafeArea: true,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||||
),
|
),
|
||||||
@@ -99,6 +120,52 @@ class _AddPostScreenState extends State<AddPostScreen> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (isEditing) {
|
||||||
|
String imageUrl = _existingImageUrl ?? '';
|
||||||
|
|
||||||
|
if (_selectedImage != null) {
|
||||||
|
// If a new image was chosen, remove the old one from storage if it existed
|
||||||
|
if (widget.postToEdit!.imageUrls.isNotEmpty) {
|
||||||
|
for (final oldUrl in widget.postToEdit!.imageUrls) {
|
||||||
|
try {
|
||||||
|
await _storageService.deleteFileByUrl(oldUrl);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imageUrl = await _storageService.uploadFile(
|
||||||
|
file: _selectedImage!,
|
||||||
|
collectionName: widget.collectionName,
|
||||||
|
optimize: true,
|
||||||
|
);
|
||||||
|
} else if (_existingImageUrl == null &&
|
||||||
|
widget.postToEdit!.imageUrls.isNotEmpty) {
|
||||||
|
// Existing image was deleted by the user
|
||||||
|
for (final oldUrl in widget.postToEdit!.imageUrls) {
|
||||||
|
try {
|
||||||
|
await _storageService.deleteFileByUrl(oldUrl);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final updatedPost = widget.postToEdit!.copyWith(
|
||||||
|
title: _titleController.text.trim(),
|
||||||
|
description: _descriptionController.text.trim(),
|
||||||
|
imageUrl: imageUrl,
|
||||||
|
imageUrls: imageUrl.isNotEmpty ? [imageUrl] : [],
|
||||||
|
);
|
||||||
|
|
||||||
|
await _firestoreService.updatePost(widget.collectionName, updatedPost);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Eintrag erfolgreich aktualisiert!'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Navigator.pop(context, updatedPost);
|
||||||
|
} else {
|
||||||
final currentUserId = FirebaseAuth.instance.currentUser?.uid;
|
final currentUserId = FirebaseAuth.instance.currentUser?.uid;
|
||||||
String imageUrl = '';
|
String imageUrl = '';
|
||||||
|
|
||||||
@@ -130,7 +197,8 @@ class _AddPostScreenState extends State<AddPostScreen> {
|
|||||||
backgroundColor: Colors.green,
|
backgroundColor: Colors.green,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context, newPost);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@@ -152,10 +220,15 @@ class _AddPostScreenState extends State<AddPostScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(widget.screenTitle),
|
title: Text(isEditing ? 'Beitrag bearbeiten' : widget.screenTitle),
|
||||||
),
|
),
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: EdgeInsets.only(
|
||||||
|
left: 16.0,
|
||||||
|
right: 16.0,
|
||||||
|
top: 16.0,
|
||||||
|
bottom: MediaQuery.paddingOf(context).bottom + 24.0,
|
||||||
|
),
|
||||||
child: Form(
|
child: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -212,12 +285,68 @@ class _AddPostScreenState extends State<AddPostScreen> {
|
|||||||
backgroundColor: Colors.black54,
|
backgroundColor: Colors.black54,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: const Icon(Icons.close, color: Colors.white),
|
icon: const Icon(Icons.close, color: Colors.white),
|
||||||
onPressed: () => setState(() => _selectedImage = null),
|
onPressed: () =>
|
||||||
|
setState(() => _selectedImage = null),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
else if (_existingImageUrl != null &&
|
||||||
|
_existingImageUrl!.isNotEmpty)
|
||||||
|
Stack(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
height: 180,
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(color: Colors.grey.shade400),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: _existingImageUrl!,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
placeholder: (ctx, url) => const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
errorWidget: (ctx, url, err) => Container(
|
||||||
|
color: Colors.grey.shade200,
|
||||||
|
child: const Icon(Icons.broken_image, size: 40),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
child: CircleAvatar(
|
||||||
|
backgroundColor: Colors.black54,
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.close, color: Colors.white),
|
||||||
|
tooltip: 'Bild entfernen',
|
||||||
|
onPressed: () =>
|
||||||
|
setState(() => _existingImageUrl = null),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 8,
|
||||||
|
right: 8,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.black54,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10, vertical: 6),
|
||||||
|
),
|
||||||
|
onPressed: _showImageSourceModal,
|
||||||
|
icon: const Icon(Icons.camera_alt, size: 16),
|
||||||
|
label: const Text('Ersetzen',
|
||||||
|
style: TextStyle(fontSize: 12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
else
|
else
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: _showImageSourceModal,
|
onTap: _showImageSourceModal,
|
||||||
@@ -232,9 +361,11 @@ class _AddPostScreenState extends State<AddPostScreen> {
|
|||||||
child: const Column(
|
child: const Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.add_photo_alternate, size: 36, color: Colors.grey),
|
Icon(Icons.add_photo_alternate,
|
||||||
|
size: 36, color: Colors.grey),
|
||||||
SizedBox(height: 6),
|
SizedBox(height: 6),
|
||||||
Text('Foto hinzufügen', style: TextStyle(color: Colors.black54)),
|
Text('Foto hinzufügen',
|
||||||
|
style: TextStyle(color: Colors.black54)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -252,9 +383,13 @@ class _AddPostScreenState extends State<AddPostScreen> {
|
|||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
strokeWidth: 2, color: Colors.white),
|
strokeWidth: 2, color: Colors.white),
|
||||||
)
|
)
|
||||||
: const Icon(Icons.send),
|
: Icon(isEditing ? Icons.save : Icons.send),
|
||||||
label: Text(
|
label: Text(
|
||||||
_isSaving ? 'Wird gespeichert...' : 'Veröffentlichen',
|
_isSaving
|
||||||
|
? 'Wird gespeichert...'
|
||||||
|
: (isEditing
|
||||||
|
? 'Änderungen speichern'
|
||||||
|
: 'Veröffentlichen'),
|
||||||
style: const TextStyle(fontSize: 16),
|
style: const TextStyle(fontSize: 16),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -89,12 +89,18 @@ class _AdminMessagesScreenState extends State<AdminMessagesScreen> {
|
|||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||||
),
|
),
|
||||||
builder: (ctx) => StatefulBuilder(
|
builder: (ctx) => StatefulBuilder(
|
||||||
builder: (ctx, setModalState) => Padding(
|
builder: (ctx, setModalState) {
|
||||||
|
final bottomInset = MediaQuery.viewPaddingOf(ctx).bottom > 0
|
||||||
|
? MediaQuery.viewPaddingOf(ctx).bottom
|
||||||
|
: MediaQuery.paddingOf(ctx).bottom;
|
||||||
|
final keyboardInset = MediaQuery.viewInsetsOf(ctx).bottom;
|
||||||
|
|
||||||
|
return Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
left: 20,
|
left: 20,
|
||||||
right: 20,
|
right: 20,
|
||||||
top: 20,
|
top: 20,
|
||||||
bottom: MediaQuery.of(ctx).viewInsets.bottom + 20,
|
bottom: bottomInset + keyboardInset + 20,
|
||||||
),
|
),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -269,7 +275,8 @@ class _AdminMessagesScreenState extends State<AdminMessagesScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -284,7 +291,7 @@ class _AdminMessagesScreenState extends State<AdminMessagesScreen> {
|
|||||||
return Expanded(
|
return Expanded(
|
||||||
child: OutlinedButton(
|
child: OutlinedButton(
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
backgroundColor: isCurrent ? color.withOpacity(0.15) : null,
|
backgroundColor: isCurrent ? color.withValues(alpha: 0.15) : null,
|
||||||
side: BorderSide(
|
side: BorderSide(
|
||||||
color: isCurrent ? color : Colors.grey.shade300,
|
color: isCurrent ? color : Colors.grey.shade300,
|
||||||
width: isCurrent ? 2 : 1,
|
width: isCurrent ? 2 : 1,
|
||||||
@@ -332,7 +339,7 @@ class _AdminMessagesScreenState extends State<AdminMessagesScreen> {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: bg,
|
color: bg,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(color: fg.withOpacity(0.3)),
|
border: Border.all(color: fg.withValues(alpha: 0.3)),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
status,
|
status,
|
||||||
@@ -495,6 +502,9 @@ class _AdminMessagesScreenState extends State<AdminMessagesScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ListView.separated(
|
return ListView.separated(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
bottom: MediaQuery.paddingOf(context).bottom + 20,
|
||||||
|
),
|
||||||
itemCount: filteredDocs.length,
|
itemCount: filteredDocs.length,
|
||||||
separatorBuilder: (ctx, i) => const Divider(height: 1),
|
separatorBuilder: (ctx, i) => const Divider(height: 1),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
|
|||||||
@@ -291,7 +291,7 @@ class _AuthDialogState extends State<AuthDialog>
|
|||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
'assets/images/logo.png',
|
'assets/images/logo.png',
|
||||||
height: 28,
|
height: 28,
|
||||||
errorBuilder: (_, __, ___) => const Icon(
|
errorBuilder: (_, _, _) => const Icon(
|
||||||
Icons.campaign,
|
Icons.campaign,
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
),
|
),
|
||||||
|
|||||||
+277
-53
@@ -1,42 +1,177 @@
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import '../models/post_item.dart';
|
import '../models/post_item.dart';
|
||||||
|
import '../services/auth_service.dart';
|
||||||
|
import '../services/firestore_service.dart';
|
||||||
import '../services/pdf_service.dart';
|
import '../services/pdf_service.dart';
|
||||||
|
import '../services/storage_service.dart';
|
||||||
|
import 'add_issue_screen.dart';
|
||||||
|
import 'add_post_screen.dart';
|
||||||
import 'pdf_viewer_screen.dart';
|
import 'pdf_viewer_screen.dart';
|
||||||
|
|
||||||
class DetailScreen extends StatelessWidget {
|
class DetailScreen extends StatefulWidget {
|
||||||
final PostItem item;
|
final PostItem item;
|
||||||
|
final String? collectionName;
|
||||||
|
|
||||||
const DetailScreen({super.key, required this.item});
|
const DetailScreen({
|
||||||
|
super.key,
|
||||||
|
required this.item,
|
||||||
|
this.collectionName,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DetailScreen> createState() => _DetailScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DetailScreenState extends State<DetailScreen> {
|
||||||
|
late PostItem _item;
|
||||||
|
int _currentImageIndex = 0;
|
||||||
|
final PageController _pageController = PageController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_item = widget.item;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_pageController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _editItem() async {
|
||||||
|
final collection = widget.collectionName ??
|
||||||
|
(_item.street != null ? 'Issues' : 'CitizensForum');
|
||||||
|
PostItem? updated;
|
||||||
|
if (collection == 'Issues' || _item.street != null) {
|
||||||
|
updated = await Navigator.push<PostItem>(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (ctx) => AddIssueScreen(issueToEdit: _item),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
updated = await Navigator.push<PostItem>(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (ctx) => AddPostScreen(
|
||||||
|
collectionName: collection,
|
||||||
|
screenTitle: 'Beitrag bearbeiten',
|
||||||
|
postToEdit: _item,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (updated != null && mounted) {
|
||||||
|
setState(() {
|
||||||
|
_item = updated!;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _deleteItem() async {
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogCtx) => AlertDialog(
|
||||||
|
title: const Text('Eintrag löschen?'),
|
||||||
|
content: const Text(
|
||||||
|
'Möchten Sie diesen Eintrag wirklich unwiderruflich löschen?'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(dialogCtx, false),
|
||||||
|
child: const Text('Abbrechen'),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||||||
|
onPressed: () => Navigator.pop(dialogCtx, true),
|
||||||
|
child: const Text('Löschen'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmed != true || !mounted) return;
|
||||||
|
|
||||||
|
final collection = widget.collectionName ??
|
||||||
|
(_item.street != null ? 'Issues' : 'News');
|
||||||
|
try {
|
||||||
|
final storage = StorageService();
|
||||||
|
final firestore = FirestoreService();
|
||||||
|
for (final url in _item.imageUrls) {
|
||||||
|
try {
|
||||||
|
await storage.deleteFileByUrl(url);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
await firestore.deletePost(collection, _item.id);
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Eintrag gelöscht.')),
|
||||||
|
);
|
||||||
|
Navigator.pop(context);
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Fehler beim Löschen: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final auth = Provider.of<AuthService>(context);
|
||||||
|
final currentUser = auth.user;
|
||||||
|
final isAuthor = currentUser != null && _item.authorUid == currentUser.uid;
|
||||||
|
final canEdit = !_item.isAussendung && (isAuthor || auth.isAdmin);
|
||||||
|
final canDelete = !_item.isAussendung && (isAuthor || auth.isAdmin);
|
||||||
|
final isIssue = widget.collectionName == 'Issues' || _item.street != null;
|
||||||
|
|
||||||
final formattedDate =
|
final formattedDate =
|
||||||
DateFormat('dd.MM.yyyy HH:mm').format(item.createdDate);
|
DateFormat('dd.MM.yyyy HH:mm').format(_item.createdDate);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(item.title, overflow: TextOverflow.ellipsis),
|
title: Text(_item.title, overflow: TextOverflow.ellipsis),
|
||||||
actions: [
|
actions: [
|
||||||
|
if (canEdit)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.edit),
|
||||||
|
tooltip: 'Beitrag bearbeiten',
|
||||||
|
onPressed: _editItem,
|
||||||
|
),
|
||||||
|
if (canDelete)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.delete_outline),
|
||||||
|
tooltip: 'Beitrag löschen',
|
||||||
|
onPressed: _deleteItem,
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.picture_as_pdf),
|
icon: const Icon(Icons.picture_as_pdf),
|
||||||
tooltip: 'Als PDF drucken / teilen',
|
tooltip: 'Als PDF drucken / teilen',
|
||||||
onPressed: () => PdfService.printSinglePost(item),
|
onPressed: () => PdfService.printSinglePost(_item),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: EdgeInsets.only(
|
||||||
|
left: 16.0,
|
||||||
|
right: 16.0,
|
||||||
|
top: 16.0,
|
||||||
|
bottom: MediaQuery.paddingOf(context).bottom + 24.0,
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (item.imageUrls.isNotEmpty)
|
if (_item.imageUrls.isNotEmpty) ...[
|
||||||
|
if (_item.imageUrls.length == 1)
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
imageUrl: item.imageUrls.first,
|
imageUrl: _item.imageUrls.first,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
|
height: 240,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
placeholder: (context, url) => const Center(
|
placeholder: (context, url) => const Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -45,15 +180,117 @@ class DetailScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
errorWidget: (context, url, error) => Container(
|
errorWidget: (context, url, error) => Container(
|
||||||
height: 180,
|
height: 200,
|
||||||
color: Colors.grey.shade200,
|
color: Colors.grey.shade200,
|
||||||
child: const Icon(Icons.image_not_supported, size: 48),
|
child: const Icon(Icons.image_not_supported, size: 48),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Stack(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
children: [
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 260,
|
||||||
|
width: double.infinity,
|
||||||
|
child: PageView.builder(
|
||||||
|
controller: _pageController,
|
||||||
|
itemCount: _item.imageUrls.length,
|
||||||
|
onPageChanged: (index) {
|
||||||
|
setState(() {
|
||||||
|
_currentImageIndex = index;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return CachedNetworkImage(
|
||||||
|
imageUrl: _item.imageUrls[index],
|
||||||
|
width: double.infinity,
|
||||||
|
height: 260,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
placeholder: (context, url) => const Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(32.0),
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
errorWidget: (context, url, error) => Container(
|
||||||
|
height: 260,
|
||||||
|
color: Colors.grey.shade200,
|
||||||
|
child: const Icon(Icons.image_not_supported,
|
||||||
|
size: 48),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Page indicator dots
|
||||||
|
Positioned(
|
||||||
|
bottom: 10,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black.withValues(alpha: 0.6),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: List.generate(
|
||||||
|
_item.imageUrls.length,
|
||||||
|
(i) => Container(
|
||||||
|
margin:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 3),
|
||||||
|
width: _currentImageIndex == i ? 16 : 6,
|
||||||
|
height: 6,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _currentImageIndex == i
|
||||||
|
? Colors.white
|
||||||
|
: Colors.white54,
|
||||||
|
borderRadius: BorderRadius.circular(3),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Photo counter badge (e.g. 1 / 2)
|
||||||
|
Positioned(
|
||||||
|
top: 10,
|
||||||
|
right: 10,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black.withValues(alpha: 0.6),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.photo_library,
|
||||||
|
color: Colors.white, size: 14),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'${_currentImageIndex + 1} / ${_item.imageUrls.length}',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
Text(
|
Text(
|
||||||
item.title,
|
_item.title,
|
||||||
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
|
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@@ -65,48 +302,63 @@ class DetailScreen extends StatelessWidget {
|
|||||||
formattedDate,
|
formattedDate,
|
||||||
style: TextStyle(fontSize: 13, color: Colors.grey.shade700),
|
style: TextStyle(fontSize: 13, color: Colors.grey.shade700),
|
||||||
),
|
),
|
||||||
if (item.status != null) ...[
|
if (isIssue && _item.status != null) ...[
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Container(
|
Container(
|
||||||
padding:
|
padding: const EdgeInsets.symmetric(
|
||||||
const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
horizontal: 10, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.blue.shade50,
|
color: _item.status == 'Erledigt'
|
||||||
|
? Colors.green.shade50
|
||||||
|
: (_item.status == 'In Bearbeitung'
|
||||||
|
? Colors.blue.shade50
|
||||||
|
: Colors.orange.shade50),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(color: Colors.blue.shade200),
|
border: Border.all(
|
||||||
|
color: _item.status == 'Erledigt'
|
||||||
|
? Colors.green.shade200
|
||||||
|
: (_item.status == 'In Bearbeitung'
|
||||||
|
? Colors.blue.shade200
|
||||||
|
: Colors.orange.shade200),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
item.status!,
|
_item.status!,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.blue.shade800),
|
color: _item.status == 'Erledigt'
|
||||||
|
? Colors.green.shade800
|
||||||
|
: (_item.status == 'In Bearbeitung'
|
||||||
|
? Colors.blue.shade800
|
||||||
|
: Colors.orange.shade900),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (item.street != null && item.street!.isNotEmpty) ...[
|
if (_item.street != null && _item.street!.isNotEmpty) ...[
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.location_on, size: 16, color: Colors.redAccent),
|
const Icon(Icons.location_on, size: 16, color: Colors.redAccent),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Text(
|
Text(
|
||||||
'Örtlichkeit: ${item.street}',
|
'Örtlichkeit: ${_item.street}',
|
||||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
if (item.category != null && item.category!.isNotEmpty) ...[
|
if (_item.category != null && _item.category!.isNotEmpty) ...[
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.category, size: 16, color: Colors.orange),
|
const Icon(Icons.category, size: 16, color: Colors.orange),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Text(
|
Text(
|
||||||
'Kategorie: ${item.category}',
|
'Kategorie: ${_item.category}',
|
||||||
style: TextStyle(color: Colors.grey.shade800),
|
style: TextStyle(color: Colors.grey.shade800),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -121,10 +373,10 @@ class DetailScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
item.description,
|
_item.description,
|
||||||
style: const TextStyle(fontSize: 15, height: 1.5),
|
style: const TextStyle(fontSize: 15, height: 1.5),
|
||||||
),
|
),
|
||||||
if (item.pdfUrl != null && item.pdfUrl!.isNotEmpty) ...[
|
if (_item.pdfUrl != null && _item.pdfUrl!.isNotEmpty) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Card(
|
Card(
|
||||||
color: Colors.red.shade50,
|
color: Colors.red.shade50,
|
||||||
@@ -150,8 +402,8 @@ class DetailScreen extends StatelessWidget {
|
|||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (ctx) => PdfViewerScreen(
|
builder: (ctx) => PdfViewerScreen(
|
||||||
title: item.title,
|
title: _item.title,
|
||||||
pdfUrl: item.pdfUrl!,
|
pdfUrl: _item.pdfUrl!,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -160,34 +412,6 @@ class DetailScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
// Additional images gallery if there are multiple images
|
|
||||||
if (item.imageUrls.length > 1) ...[
|
|
||||||
const Text(
|
|
||||||
'Weitere Fotos',
|
|
||||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
SizedBox(
|
|
||||||
height: 120,
|
|
||||||
child: ListView.separated(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
itemCount: item.imageUrls.length - 1,
|
|
||||||
separatorBuilder: (ctx, i) => const SizedBox(width: 8),
|
|
||||||
itemBuilder: (ctx, index) {
|
|
||||||
final url = item.imageUrls[index + 1];
|
|
||||||
return ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
child: CachedNetworkImage(
|
|
||||||
imageUrl: url,
|
|
||||||
width: 120,
|
|
||||||
height: 120,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -19,37 +19,63 @@ class PdfViewerScreen extends StatefulWidget {
|
|||||||
State<PdfViewerScreen> createState() => _PdfViewerScreenState();
|
State<PdfViewerScreen> createState() => _PdfViewerScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PdfViewerScreenState extends State<PdfViewerScreen> {
|
class _PdfViewerScreenState extends State<PdfViewerScreen>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
final TransformationController _transformationController =
|
final TransformationController _transformationController =
|
||||||
TransformationController();
|
TransformationController();
|
||||||
double _zoomLevel = 1.0;
|
late AnimationController _animationController;
|
||||||
|
Animation<Matrix4>? _zoomAnimation;
|
||||||
|
TapDownDetails? _doubleTapDetails;
|
||||||
Uint8List? _cachedBytes;
|
Uint8List? _cachedBytes;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_animationController = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 250),
|
||||||
|
)..addListener(() {
|
||||||
|
if (_zoomAnimation != null) {
|
||||||
|
_transformationController.value = _zoomAnimation!.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_animationController.dispose();
|
||||||
_transformationController.dispose();
|
_transformationController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _zoomIn() {
|
void _handleDoubleTap() {
|
||||||
setState(() {
|
if (_animationController.isAnimating) return;
|
||||||
_zoomLevel = (_zoomLevel * 1.3).clamp(0.8, 5.0);
|
|
||||||
_transformationController.value = Matrix4.identity()..scale(_zoomLevel);
|
final currentMatrix = _transformationController.value;
|
||||||
});
|
final currentScale = currentMatrix.getMaxScaleOnAxis();
|
||||||
|
|
||||||
|
Matrix4 targetMatrix;
|
||||||
|
if (currentScale > 1.05) {
|
||||||
|
// Zoomed in -> smoothly animate back to standard size (scale 1.0)
|
||||||
|
targetMatrix = Matrix4.identity();
|
||||||
|
} else {
|
||||||
|
// At standard size -> zoom in to tapped point (2.5x)
|
||||||
|
final position = _doubleTapDetails?.localPosition ?? Offset.zero;
|
||||||
|
final scenePoint = _transformationController.toScene(position);
|
||||||
|
final x = position.dx - scenePoint.dx * 2.5;
|
||||||
|
final y = position.dy - scenePoint.dy * 2.5;
|
||||||
|
targetMatrix = Matrix4.diagonal3Values(2.5, 2.5, 1.0)
|
||||||
|
..setTranslationRaw(x, y, 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _zoomOut() {
|
_zoomAnimation = Matrix4Tween(
|
||||||
setState(() {
|
begin: currentMatrix,
|
||||||
_zoomLevel = (_zoomLevel / 1.3).clamp(0.8, 5.0);
|
end: targetMatrix,
|
||||||
_transformationController.value = Matrix4.identity()..scale(_zoomLevel);
|
).animate(CurvedAnimation(
|
||||||
});
|
parent: _animationController,
|
||||||
}
|
curve: Curves.easeOutCubic,
|
||||||
|
));
|
||||||
void _resetZoom() {
|
_animationController.forward(from: 0);
|
||||||
setState(() {
|
|
||||||
_zoomLevel = 1.0;
|
|
||||||
_transformationController.value = Matrix4.identity();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _openExternal() async {
|
Future<void> _openExternal() async {
|
||||||
@@ -110,9 +136,9 @@ class _PdfViewerScreenState extends State<PdfViewerScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: Stack(
|
body: SafeArea(
|
||||||
children: [
|
top: false,
|
||||||
PdfPreview.builder(
|
child: PdfPreview.builder(
|
||||||
build: (PdfPageFormat format) async {
|
build: (PdfPageFormat format) async {
|
||||||
if (_cachedBytes != null) return _cachedBytes!;
|
if (_cachedBytes != null) return _cachedBytes!;
|
||||||
final response = await http
|
final response = await http
|
||||||
@@ -166,25 +192,27 @@ class _PdfViewerScreenState extends State<PdfViewerScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
pagesBuilder: (context, pages) {
|
pagesBuilder: (context, pages) {
|
||||||
return InteractiveViewer(
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final viewportWidth = constraints.maxWidth;
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
onDoubleTapDown: (details) => _doubleTapDetails = details,
|
||||||
|
onDoubleTap: _handleDoubleTap,
|
||||||
|
child: InteractiveViewer(
|
||||||
transformationController: _transformationController,
|
transformationController: _transformationController,
|
||||||
minScale: 0.8,
|
minScale: 1.0,
|
||||||
maxScale: 5.0,
|
maxScale: 5.0,
|
||||||
panEnabled: true,
|
panEnabled: true,
|
||||||
scaleEnabled: true,
|
scaleEnabled: true,
|
||||||
onInteractionEnd: (details) {
|
constrained: false,
|
||||||
final scale =
|
boundaryMargin: const EdgeInsets.symmetric(
|
||||||
_transformationController.value.getMaxScaleOnAxis();
|
horizontal: 48, vertical: 80),
|
||||||
if (scale != _zoomLevel) {
|
child: SizedBox(
|
||||||
setState(() {
|
width: viewportWidth,
|
||||||
_zoomLevel = scale;
|
child: Padding(
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 8.0, vertical: 16.0),
|
horizontal: 8.0, vertical: 16.0),
|
||||||
child: Center(
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@@ -196,7 +224,8 @@ class _PdfViewerScreenState extends State<PdfViewerScreen> {
|
|||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.15),
|
color:
|
||||||
|
Colors.black.withValues(alpha: 0.15),
|
||||||
blurRadius: 10,
|
blurRadius: 10,
|
||||||
offset: const Offset(0, 3),
|
offset: const Offset(0, 3),
|
||||||
),
|
),
|
||||||
@@ -224,59 +253,12 @@ class _PdfViewerScreenState extends State<PdfViewerScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
// Floating Zoom Controls Overlay
|
|
||||||
Positioned(
|
|
||||||
right: 16,
|
|
||||||
bottom: 24,
|
|
||||||
child: Card(
|
|
||||||
elevation: 4,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(24)),
|
|
||||||
color: Colors.white.withOpacity(0.92),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 2),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.zoom_in, color: Colors.black87),
|
|
||||||
tooltip: 'Vergrößern (+)',
|
|
||||||
onPressed: _zoomIn,
|
|
||||||
),
|
|
||||||
InkWell(
|
|
||||||
onTap: _resetZoom,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
vertical: 4, horizontal: 6),
|
|
||||||
child: Text(
|
|
||||||
'${(_zoomLevel * 100).round()}%',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.blue,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.zoom_out, color: Colors.black87),
|
|
||||||
tooltip: 'Verkleinern (-)',
|
|
||||||
onPressed: _zoomOut,
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.restart_alt, size: 20, color: Colors.grey),
|
|
||||||
tooltip: 'Zoom zurücksetzen (1:1)',
|
|
||||||
onPressed: _resetZoom,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,6 +129,30 @@ class _PostListScreenState extends State<PostListScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _editPost(PostItem item) {
|
||||||
|
if (widget.collectionName == 'Issues') {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (ctx) => AddIssueScreen(issueToEdit: item),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (ctx) => AddPostScreen(
|
||||||
|
collectionName: widget.collectionName,
|
||||||
|
screenTitle: widget.collectionName == 'News'
|
||||||
|
? 'Nachricht bearbeiten'
|
||||||
|
: 'Idee bearbeiten',
|
||||||
|
postToEdit: item,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _onAddNewPressed() async {
|
Future<void> _onAddNewPressed() async {
|
||||||
final auth = Provider.of<AuthService>(context, listen: false);
|
final auth = Provider.of<AuthService>(context, listen: false);
|
||||||
|
|
||||||
@@ -402,8 +426,11 @@ class _PostListScreenState extends State<PostListScreen> {
|
|||||||
final isAuthor =
|
final isAuthor =
|
||||||
currentUser != null && item.authorUid == currentUser.uid;
|
currentUser != null && item.authorUid == currentUser.uid;
|
||||||
final canDelete = !item.isAussendung && (isAuthor || auth.isAdmin);
|
final canDelete = !item.isAussendung && (isAuthor || auth.isAdmin);
|
||||||
|
final canEdit = !item.isAussendung && (isAuthor || auth.isAdmin);
|
||||||
final formattedDate =
|
final formattedDate =
|
||||||
DateFormat('dd.MM.yyyy').format(item.createdDate);
|
DateFormat('dd.MM.yyyy').format(item.createdDate);
|
||||||
|
final isIssue =
|
||||||
|
widget.collectionName == 'Issues' || item.street != null;
|
||||||
|
|
||||||
Widget? leadingWidget;
|
Widget? leadingWidget;
|
||||||
if (item.imageUrl.isNotEmpty) {
|
if (item.imageUrl.isNotEmpty) {
|
||||||
@@ -525,7 +552,10 @@ class _PostListScreenState extends State<PostListScreen> {
|
|||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (ctx) => DetailScreen(item: item),
|
builder: (ctx) => DetailScreen(
|
||||||
|
item: item,
|
||||||
|
collectionName: widget.collectionName,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -535,7 +565,73 @@ class _PostListScreenState extends State<PostListScreen> {
|
|||||||
trailing: item.isAussendung
|
trailing: item.isAussendung
|
||||||
? Icon(Icons.arrow_forward_ios,
|
? Icon(Icons.arrow_forward_ios,
|
||||||
size: 14, color: Colors.grey.shade400)
|
size: 14, color: Colors.grey.shade400)
|
||||||
: null,
|
: (_selectedItems.isNotEmpty
|
||||||
|
? null
|
||||||
|
: ((canEdit || canDelete)
|
||||||
|
? PopupMenuButton<String>(
|
||||||
|
icon: const Icon(Icons.more_vert,
|
||||||
|
size: 20, color: Colors.black54),
|
||||||
|
tooltip: 'Optionen',
|
||||||
|
onSelected: (value) async {
|
||||||
|
if (value == 'edit') {
|
||||||
|
_editPost(item);
|
||||||
|
} else if (value == 'delete') {
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogCtx) => AlertDialog(
|
||||||
|
title: const Text('Eintrag löschen?'),
|
||||||
|
content: const Text(
|
||||||
|
'Möchten Sie diesen Eintrag wirklich unwiderruflich löschen?'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () =>
|
||||||
|
Navigator.pop(dialogCtx, false),
|
||||||
|
child: const Text('Abbrechen'),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.red),
|
||||||
|
onPressed: () =>
|
||||||
|
Navigator.pop(dialogCtx, true),
|
||||||
|
child: const Text('Löschen'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (confirmed == true) {
|
||||||
|
_deletePost(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemBuilder: (ctx) => [
|
||||||
|
if (canEdit)
|
||||||
|
const PopupMenuItem<String>(
|
||||||
|
value: 'edit',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.edit_outlined, size: 20),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text('Bearbeiten'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (canDelete)
|
||||||
|
const PopupMenuItem<String>(
|
||||||
|
value: 'delete',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.delete_outline,
|
||||||
|
size: 20, color: Colors.red),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text('Löschen',
|
||||||
|
style:
|
||||||
|
TextStyle(color: Colors.red)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: null)),
|
||||||
title: Row(
|
title: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
@@ -563,7 +659,64 @@ class _PostListScreenState extends State<PostListScreen> {
|
|||||||
subtitle: Column(
|
subtitle: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (item.street != null && item.street!.isNotEmpty) ...[
|
if (isIssue && item.status != null && item.status!.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 3),
|
||||||
|
Wrap(
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
spacing: 6,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 6, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: item.status == 'Erledigt'
|
||||||
|
? Colors.green.shade50
|
||||||
|
: (item.status == 'In Bearbeitung'
|
||||||
|
? Colors.blue.shade50
|
||||||
|
: Colors.orange.shade50),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
border: Border.all(
|
||||||
|
color: item.status == 'Erledigt'
|
||||||
|
? Colors.green.shade300
|
||||||
|
: (item.status == 'In Bearbeitung'
|
||||||
|
? Colors.blue.shade300
|
||||||
|
: Colors.orange.shade300),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
item.status!,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: item.status == 'Erledigt'
|
||||||
|
? Colors.green.shade800
|
||||||
|
: (item.status == 'In Bearbeitung'
|
||||||
|
? Colors.blue.shade800
|
||||||
|
: Colors.orange.shade900),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (item.street != null && item.street!.isNotEmpty)
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.location_on,
|
||||||
|
size: 12, color: Colors.redAccent),
|
||||||
|
const SizedBox(width: 2),
|
||||||
|
Text(
|
||||||
|
item.street!,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
] else if (item.street != null && item.street!.isNotEmpty) ...[
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -365,6 +365,9 @@ class _UserManagementScreenState extends State<UserManagementScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ListView.separated(
|
return ListView.separated(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
bottom: MediaQuery.paddingOf(context).bottom + 20,
|
||||||
|
),
|
||||||
itemCount: filteredUsers.length,
|
itemCount: filteredUsers.length,
|
||||||
separatorBuilder: (ctx, i) => const Divider(height: 1),
|
separatorBuilder: (ctx, i) => const Divider(height: 1),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
|
|||||||
@@ -43,6 +43,14 @@ class FirestoreService {
|
|||||||
await _firestore.collection(collectionName).doc(documentId).delete();
|
await _firestore.collection(collectionName).doc(documentId).delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update an existing post by ID
|
||||||
|
Future<void> updatePost(String collectionName, PostItem item) async {
|
||||||
|
await _firestore
|
||||||
|
.collection(collectionName)
|
||||||
|
.doc(item.id)
|
||||||
|
.update(item.toFirestore());
|
||||||
|
}
|
||||||
|
|
||||||
/// Send a contact form submission to Firestore
|
/// Send a contact form submission to Firestore
|
||||||
Future<void> submitContactMessage({
|
Future<void> submitContactMessage({
|
||||||
required String firstName,
|
required String firstName,
|
||||||
@@ -118,8 +126,8 @@ class FirestoreService {
|
|||||||
'firstName': firstName,
|
'firstName': firstName,
|
||||||
if (lastName != null && lastName.isNotEmpty)
|
if (lastName != null && lastName.isNotEmpty)
|
||||||
'lastName': lastName,
|
'lastName': lastName,
|
||||||
if (emailVerified != null) 'emailVerified': emailVerified,
|
'emailVerified': ?emailVerified,
|
||||||
if (isAdmin != null) 'isAdmin': isAdmin,
|
'isAdmin': ?isAdmin,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!doc.exists) {
|
if (!doc.exists) {
|
||||||
|
|||||||
+102
-14
@@ -35,8 +35,15 @@ class PdfService {
|
|||||||
/// Print single post item as PDF
|
/// Print single post item as PDF
|
||||||
static Future<void> printSinglePost(PostItem item) async {
|
static Future<void> printSinglePost(PostItem item) async {
|
||||||
final pdf = pw.Document();
|
final pdf = pw.Document();
|
||||||
final formattedDate = DateFormat('dd.MM.yyyy HH:mm').format(item.createdDate);
|
final formattedDate =
|
||||||
final pw.ImageProvider? imageProvider = await _fetchImage(item.imageUrl);
|
DateFormat('dd.MM.yyyy HH:mm').format(item.createdDate);
|
||||||
|
|
||||||
|
final List<String> urlsToFetch = item.imageUrls.isNotEmpty
|
||||||
|
? item.imageUrls
|
||||||
|
: (item.imageUrl.isNotEmpty ? [item.imageUrl] : []);
|
||||||
|
final results =
|
||||||
|
await Future.wait(urlsToFetch.map((url) => _fetchImage(url)));
|
||||||
|
final imageProviders = results.whereType<pw.ImageProvider>().toList();
|
||||||
|
|
||||||
pdf.addPage(
|
pdf.addPage(
|
||||||
pw.MultiPage(
|
pw.MultiPage(
|
||||||
@@ -58,7 +65,8 @@ class PdfService {
|
|||||||
),
|
),
|
||||||
pw.Text(
|
pw.Text(
|
||||||
formattedDate,
|
formattedDate,
|
||||||
style: const pw.TextStyle(fontSize: 12, color: PdfColors.grey700),
|
style:
|
||||||
|
const pw.TextStyle(fontSize: 12, color: PdfColors.grey700),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -68,6 +76,53 @@ class PdfService {
|
|||||||
item.title,
|
item.title,
|
||||||
style: pw.TextStyle(fontSize: 22, fontWeight: pw.FontWeight.bold),
|
style: pw.TextStyle(fontSize: 22, fontWeight: pw.FontWeight.bold),
|
||||||
),
|
),
|
||||||
|
if (item.status != null && item.status!.isNotEmpty) ...[
|
||||||
|
pw.SizedBox(height: 6),
|
||||||
|
pw.Row(
|
||||||
|
children: [
|
||||||
|
pw.Text(
|
||||||
|
'Status: ',
|
||||||
|
style: pw.TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: pw.FontWeight.bold,
|
||||||
|
color: PdfColors.grey800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pw.Container(
|
||||||
|
padding:
|
||||||
|
const pw.EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||||
|
decoration: pw.BoxDecoration(
|
||||||
|
color: item.status == 'Erledigt'
|
||||||
|
? const PdfColor.fromInt(0xFFE8F5E9)
|
||||||
|
: (item.status == 'In Bearbeitung'
|
||||||
|
? const PdfColor.fromInt(0xFFE3F2FD)
|
||||||
|
: const PdfColor.fromInt(0xFFFFF3E0)),
|
||||||
|
borderRadius:
|
||||||
|
const pw.BorderRadius.all(pw.Radius.circular(4)),
|
||||||
|
border: pw.Border.all(
|
||||||
|
color: item.status == 'Erledigt'
|
||||||
|
? const PdfColor.fromInt(0xFF81C784)
|
||||||
|
: (item.status == 'In Bearbeitung'
|
||||||
|
? const PdfColor.fromInt(0xFF64B5F6)
|
||||||
|
: const PdfColor.fromInt(0xFFFFB74D)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: pw.Text(
|
||||||
|
item.status!,
|
||||||
|
style: pw.TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: pw.FontWeight.bold,
|
||||||
|
color: item.status == 'Erledigt'
|
||||||
|
? const PdfColor.fromInt(0xFF2E7D32)
|
||||||
|
: (item.status == 'In Bearbeitung'
|
||||||
|
? const PdfColor.fromInt(0xFF1565C0)
|
||||||
|
: const PdfColor.fromInt(0xFFE65100)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
if (item.street != null && item.street!.isNotEmpty) ...[
|
if (item.street != null && item.street!.isNotEmpty) ...[
|
||||||
pw.SizedBox(height: 6),
|
pw.SizedBox(height: 6),
|
||||||
pw.Text(
|
pw.Text(
|
||||||
@@ -84,12 +139,28 @@ class PdfService {
|
|||||||
],
|
],
|
||||||
pw.Divider(thickness: 1, color: PdfColors.grey400),
|
pw.Divider(thickness: 1, color: PdfColors.grey400),
|
||||||
pw.SizedBox(height: 10),
|
pw.SizedBox(height: 10),
|
||||||
if (imageProvider != null) ...[
|
if (imageProviders.isNotEmpty) ...[
|
||||||
|
if (imageProviders.length == 1)
|
||||||
pw.Center(
|
pw.Center(
|
||||||
child: pw.Container(
|
child: pw.Container(
|
||||||
height: 220,
|
height: 220,
|
||||||
child: pw.Image(imageProvider, fit: pw.BoxFit.contain),
|
child: pw.Image(imageProviders.first, fit: pw.BoxFit.contain),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
pw.Row(
|
||||||
|
children: [
|
||||||
|
for (int i = 0; i < imageProviders.length; i++) ...[
|
||||||
|
if (i > 0) pw.SizedBox(width: 12),
|
||||||
|
pw.Expanded(
|
||||||
|
child: pw.Container(
|
||||||
|
height: 180,
|
||||||
|
child: pw.Image(imageProviders[i],
|
||||||
|
fit: pw.BoxFit.contain),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
pw.SizedBox(height: 14),
|
pw.SizedBox(height: 14),
|
||||||
],
|
],
|
||||||
@@ -123,8 +194,10 @@ class PdfService {
|
|||||||
// Preload all images in parallel
|
// Preload all images in parallel
|
||||||
final Map<String, pw.ImageProvider?> imageMap = {};
|
final Map<String, pw.ImageProvider?> imageMap = {};
|
||||||
await Future.wait(items.map((item) async {
|
await Future.wait(items.map((item) async {
|
||||||
if (item.imageUrl.isNotEmpty) {
|
final url =
|
||||||
imageMap[item.id] = await _fetchImage(item.imageUrl);
|
item.imageUrls.isNotEmpty ? item.imageUrls.first : item.imageUrl;
|
||||||
|
if (url.isNotEmpty) {
|
||||||
|
imageMap[item.id] = await _fetchImage(url);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -137,7 +210,8 @@ class PdfService {
|
|||||||
margin: const pw.EdgeInsets.only(bottom: 12),
|
margin: const pw.EdgeInsets.only(bottom: 12),
|
||||||
padding: const pw.EdgeInsets.only(bottom: 6),
|
padding: const pw.EdgeInsets.only(bottom: 6),
|
||||||
decoration: const pw.BoxDecoration(
|
decoration: const pw.BoxDecoration(
|
||||||
border: pw.Border(bottom: pw.BorderSide(color: PdfColors.blue800, width: 2)),
|
border: pw.Border(
|
||||||
|
bottom: pw.BorderSide(color: PdfColors.blue800, width: 2)),
|
||||||
),
|
),
|
||||||
child: pw.Row(
|
child: pw.Row(
|
||||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||||
@@ -152,7 +226,8 @@ class PdfService {
|
|||||||
),
|
),
|
||||||
pw.Text(
|
pw.Text(
|
||||||
'Erstellt am: ${DateFormat('dd.MM.yyyy HH:mm').format(DateTime.now())}',
|
'Erstellt am: ${DateFormat('dd.MM.yyyy HH:mm').format(DateTime.now())}',
|
||||||
style: const pw.TextStyle(fontSize: 10, color: PdfColors.grey700),
|
style:
|
||||||
|
const pw.TextStyle(fontSize: 10, color: PdfColors.grey700),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -160,7 +235,8 @@ class PdfService {
|
|||||||
build: (context) => [
|
build: (context) => [
|
||||||
pw.TableHelper.fromTextArray(
|
pw.TableHelper.fromTextArray(
|
||||||
border: pw.TableBorder.all(color: PdfColors.grey400, width: 0.5),
|
border: pw.TableBorder.all(color: PdfColors.grey400, width: 0.5),
|
||||||
headerStyle: pw.TextStyle(fontWeight: pw.FontWeight.bold, fontSize: 10),
|
headerStyle:
|
||||||
|
pw.TextStyle(fontWeight: pw.FontWeight.bold, fontSize: 10),
|
||||||
headerDecoration: const pw.BoxDecoration(color: PdfColors.grey200),
|
headerDecoration: const pw.BoxDecoration(color: PdfColors.grey200),
|
||||||
cellHeight: 65,
|
cellHeight: 65,
|
||||||
cellAlignments: {
|
cellAlignments: {
|
||||||
@@ -170,20 +246,32 @@ class PdfService {
|
|||||||
3: pw.Alignment.centerLeft,
|
3: pw.Alignment.centerLeft,
|
||||||
4: pw.Alignment.center,
|
4: pw.Alignment.center,
|
||||||
},
|
},
|
||||||
headers: <String>['Datum', 'Örtlichkeit / Kat.', 'Titel & Mangel', 'Status', 'Foto'],
|
headers: <String>[
|
||||||
|
'Datum',
|
||||||
|
'Örtlichkeit / Kat.',
|
||||||
|
'Titel & Mangel',
|
||||||
|
'Status',
|
||||||
|
'Foto'
|
||||||
|
],
|
||||||
data: items.map((item) {
|
data: items.map((item) {
|
||||||
final formattedDate = DateFormat('dd.MM.yyyy').format(item.createdDate);
|
final formattedDate =
|
||||||
final locInfo = '${item.street ?? "Keine Angabe"}\n(${item.category ?? "-"})';
|
DateFormat('dd.MM.yyyy').format(item.createdDate);
|
||||||
|
final locInfo =
|
||||||
|
'${item.street ?? "Keine Angabe"}\n(${item.category ?? "-"})';
|
||||||
final descSnippet = item.description.length > 120
|
final descSnippet = item.description.length > 120
|
||||||
? '${item.description.substring(0, 120)}...'
|
? '${item.description.substring(0, 120)}...'
|
||||||
: item.description;
|
: item.description;
|
||||||
|
final imageCount = item.imageUrls.length;
|
||||||
|
final imageLabel = imageMap[item.id] != null
|
||||||
|
? (imageCount > 1 ? '$imageCount Fotos' : 'Foto vorhanden')
|
||||||
|
: '-';
|
||||||
|
|
||||||
return [
|
return [
|
||||||
formattedDate,
|
formattedDate,
|
||||||
locInfo,
|
locInfo,
|
||||||
'${item.title}\n$descSnippet',
|
'${item.title}\n$descSnippet',
|
||||||
item.status ?? 'Gemeldet',
|
item.status ?? 'Gemeldet',
|
||||||
imageMap[item.id] != null ? 'Foto vorhanden' : '-',
|
imageLabel,
|
||||||
];
|
];
|
||||||
}).toList(),
|
}).toList(),
|
||||||
),
|
),
|
||||||
|
|||||||
+259
-227
File diff suppressed because it is too large
Load Diff
+34
-82
@@ -1,109 +1,61 @@
|
|||||||
name: oggauergemeindetrommler
|
name: oggauergemeindetrommler
|
||||||
description: Gemeinde Trommler
|
description: Gemeinde Trommler
|
||||||
|
|
||||||
|
# Private app – never publish to pub.dev.
|
||||||
|
publish_to: 'none'
|
||||||
|
|
||||||
# The following line prevents the package from being accidentally published to
|
# Version + build number (build-name+build-number). Can be overridden with
|
||||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
# `flutter build ... --build-name --build-number`.
|
||||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|
||||||
|
|
||||||
# The following defines the version and build number for your application.
|
|
||||||
# A version number is three numbers separated by dots, like 1.2.43
|
|
||||||
# followed by an optional build number separated by a +.
|
|
||||||
# Both the version and the builder number may be overridden in flutter
|
|
||||||
# build by specifying --build-name and --build-number, respectively.
|
|
||||||
# In Android, build-name is used as versionName while build-number used as versionCode.
|
|
||||||
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
|
|
||||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
|
|
||||||
# Read more about iOS versioning at
|
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
|
||||||
version: 0.0.1
|
version: 0.0.1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.0.0 <4.0.0'
|
# Flutter 3.47.x ships Dart 3.13; keep the constraint in sync with the
|
||||||
|
# Flutter version pinned in the CI workflows (FLUTTER_VERSION).
|
||||||
|
sdk: ^3.11.0
|
||||||
|
flutter: '>=3.47.0'
|
||||||
|
|
||||||
# Dependencies specify other packages that your package needs in order to work.
|
|
||||||
# To automatically upgrade your package dependencies to the latest versions
|
|
||||||
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
|
||||||
# dependencies can be manually updated by changing the version numbers below to
|
|
||||||
# the latest version available on pub.dev. To see which dependencies have newer
|
|
||||||
# versions available, run `flutter pub outdated`.
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
|
# Firebase (Auth, Firestore, Storage)
|
||||||
|
firebase_core: ^4.15.0
|
||||||
|
firebase_auth: ^6.7.0
|
||||||
|
cloud_firestore: ^6.10.0
|
||||||
|
firebase_storage: ^13.6.0
|
||||||
|
|
||||||
|
# UI / state
|
||||||
|
provider: ^6.1.5
|
||||||
|
cupertino_icons: ^1.0.8
|
||||||
|
cached_network_image: ^3.4.1
|
||||||
intl: ^0.20.2
|
intl: ^0.20.2
|
||||||
|
|
||||||
|
# Media
|
||||||
|
image_picker: ^1.2.3
|
||||||
|
image: ^4.5.4
|
||||||
|
|
||||||
|
# PDF export & sharing
|
||||||
|
pdf: ^3.11.3
|
||||||
|
printing: ^5.14.3
|
||||||
|
|
||||||
# The following adds the Cupertino Icons font to your application.
|
# Misc
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
http: ^1.2.2
|
||||||
cupertino_icons: ^1.0.2
|
shared_preferences: ^2.5.3
|
||||||
image_picker: ^1.1.2
|
|
||||||
firebase_core: ^3.4.0
|
|
||||||
firebase_auth: ^5.2.0
|
|
||||||
firebase_storage: ^12.2.0
|
|
||||||
cloud_firestore: ^5.4.0
|
|
||||||
change_app_package_name: ^1.1.0
|
|
||||||
cached_network_image: ^3.2.3
|
|
||||||
image: ^4.0.16
|
|
||||||
pdf: ^3.10.1
|
|
||||||
printing: ^5.10.4
|
|
||||||
image_cropper: ^8.0.1
|
|
||||||
logging: ^1.1.1
|
|
||||||
provider: ^6.0.5
|
|
||||||
http: ^1.2.0
|
|
||||||
shared_preferences: ^2.2.2
|
|
||||||
url_launcher: ^6.3.1
|
url_launcher: ^6.3.1
|
||||||
|
|
||||||
|
# Transitive (via flutter_cache_manager / printing), pinned on purpose:
|
||||||
|
# path_provider_android >= 2.3 is implemented with `jni` and compiles native
|
||||||
|
# code (CMake + NDK). The ARM64 Gitea runner would have to run the x86_64 NDK
|
||||||
|
# toolchain through QEMU, so stay on the pure-Java implementation for now.
|
||||||
|
path_provider_android: '>=2.2.0 <2.3.0'
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
flutter_lints: ^6.0.0
|
||||||
|
|
||||||
# The "flutter_lints" package below contains a set of recommended lints to
|
|
||||||
# encourage good coding practices. The lint set provided by the package is
|
|
||||||
# activated in the `analysis_options.yaml` file located at the root of your
|
|
||||||
# package. See that file for information about deactivating specific lint
|
|
||||||
# rules and activating additional ones.
|
|
||||||
flutter_lints: ^4.0.0
|
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
|
||||||
|
|
||||||
# The following section is specific to Flutter packages.
|
|
||||||
flutter:
|
flutter:
|
||||||
|
|
||||||
# The following line ensures that the Material Icons font is
|
|
||||||
# included with your application, so that you can use the icons in
|
|
||||||
# the material Icons class.
|
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|
||||||
assets:
|
assets:
|
||||||
- assets/images/
|
- assets/images/
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
|
||||||
# https://flutter.dev/assets-and-images/#resolution-aware
|
|
||||||
|
|
||||||
# For details regarding adding assets from package dependencies, see
|
|
||||||
# https://flutter.dev/assets-and-images/#from-packages
|
|
||||||
|
|
||||||
# To add custom fonts to your application, add a fonts section here,
|
|
||||||
# in this "flutter" section. Each entry in this list should have a
|
|
||||||
# "family" key with the font family name, and a "fonts" key with a
|
|
||||||
# list giving the asset and other descriptors for the font. For
|
|
||||||
# example:
|
|
||||||
# fonts:
|
|
||||||
# - family: Schyler
|
|
||||||
# fonts:
|
|
||||||
# - asset: fonts/Schyler-Regular.ttf
|
|
||||||
# - asset: fonts/Schyler-Italic.ttf
|
|
||||||
# style: italic
|
|
||||||
# - family: Trajan Pro
|
|
||||||
# fonts:
|
|
||||||
# - asset: fonts/TrajanPro.ttf
|
|
||||||
# - asset: fonts/TrajanPro_Bold.ttf
|
|
||||||
# weight: 700
|
|
||||||
#
|
|
||||||
# For details regarding fonts from package dependencies,
|
|
||||||
# see https://flutter.dev/custom-fonts/#from-packages
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:oggauergemeindetrommler/models/post_item.dart';
|
import 'package:oggauergemeindetrommler/models/post_item.dart';
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ void main() {
|
|||||||
createdDate: now,
|
createdDate: now,
|
||||||
street: 'Hauptstraße',
|
street: 'Hauptstraße',
|
||||||
category: 'Straßenbeleuchtung',
|
category: 'Straßenbeleuchtung',
|
||||||
|
status: 'Gemeldet',
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(item.id, 'test-123');
|
expect(item.id, 'test-123');
|
||||||
@@ -23,6 +25,46 @@ void main() {
|
|||||||
expect(item.status, 'Gemeldet');
|
expect(item.status, 'Gemeldet');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('defaults status to Gemeldet only for issues and null for news/ideas', () {
|
||||||
|
final issueJson = {
|
||||||
|
'id': 'issue-1',
|
||||||
|
'title': 'Schlagloch',
|
||||||
|
'description': 'Tiefes Schlagloch',
|
||||||
|
'createdDate': '2026-09-18T10:00:00.000',
|
||||||
|
'street': 'Kirchengasse',
|
||||||
|
'category': 'Straße',
|
||||||
|
};
|
||||||
|
final issueItem = PostItem.fromJson(issueJson);
|
||||||
|
expect(issueItem.status, 'Gemeldet');
|
||||||
|
|
||||||
|
final newsJson = {
|
||||||
|
'id': 'news-1',
|
||||||
|
'title': 'Dorffest Ankündigung',
|
||||||
|
'description': 'Am Samstag findet das Fest statt.',
|
||||||
|
'createdDate': '2026-09-18T10:00:00.000',
|
||||||
|
};
|
||||||
|
final newsItem = PostItem.fromJson(newsJson);
|
||||||
|
expect(newsItem.status, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles multiple images in PostItem', () {
|
||||||
|
final item = PostItem(
|
||||||
|
id: 'multi-img-1',
|
||||||
|
title: 'Baustelle',
|
||||||
|
description: 'Zwei Fotos vorhanden',
|
||||||
|
createdDate: DateTime.now(),
|
||||||
|
imageUrls: [
|
||||||
|
'https://example.com/img1.jpg',
|
||||||
|
'https://example.com/img2.jpg',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(item.imageUrls.length, 2);
|
||||||
|
expect(item.imageUrls[0], 'https://example.com/img1.jpg');
|
||||||
|
expect(item.imageUrls[1], 'https://example.com/img2.jpg');
|
||||||
|
expect(item.imageUrl, 'https://example.com/img1.jpg');
|
||||||
|
});
|
||||||
|
|
||||||
test('serializes to Firestore map accurately', () {
|
test('serializes to Firestore map accurately', () {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final item = PostItem(
|
final item = PostItem(
|
||||||
@@ -167,4 +209,172 @@ void main() {
|
|||||||
expect(canDemote('current_admin', 'current_admin'), isFalse);
|
expect(canDemote('current_admin', 'current_admin'), isFalse);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('PDF Viewer & Navigation Overlap Tests', () {
|
||||||
|
test('verifies double-tap zoom matrix calculation and reset', () {
|
||||||
|
// Test at standard scale (1.0) -> zoom to 2.5x
|
||||||
|
final currentMatrix = Matrix4.identity();
|
||||||
|
final currentScale = currentMatrix.getMaxScaleOnAxis();
|
||||||
|
expect(currentScale, 1.0);
|
||||||
|
|
||||||
|
const tapPosition = Offset(100, 200);
|
||||||
|
Matrix4 targetMatrix;
|
||||||
|
if (currentScale > 1.05) {
|
||||||
|
targetMatrix = Matrix4.identity();
|
||||||
|
} else {
|
||||||
|
final x = -tapPosition.dx * (2.5 - 1.0);
|
||||||
|
final y = -tapPosition.dy * (2.5 - 1.0);
|
||||||
|
targetMatrix = Matrix4.diagonal3Values(2.5, 2.5, 1.0)
|
||||||
|
..setTranslationRaw(x, y, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(targetMatrix.getMaxScaleOnAxis(), 2.5);
|
||||||
|
expect(targetMatrix.getTranslation().x, -150.0);
|
||||||
|
expect(targetMatrix.getTranslation().y, -300.0);
|
||||||
|
|
||||||
|
// Test when zoomed in -> reset to standard size (scale 1.0)
|
||||||
|
final zoomedScale = targetMatrix.getMaxScaleOnAxis();
|
||||||
|
expect(zoomedScale > 1.05, isTrue);
|
||||||
|
|
||||||
|
Matrix4 resetMatrix;
|
||||||
|
if (zoomedScale > 1.05) {
|
||||||
|
resetMatrix = Matrix4.identity();
|
||||||
|
} else {
|
||||||
|
resetMatrix = targetMatrix;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(resetMatrix.getMaxScaleOnAxis(), 1.0);
|
||||||
|
expect(resetMatrix.getTranslation().x, 0.0);
|
||||||
|
expect(resetMatrix.getTranslation().y, 0.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('calculates safe bottom padding above 3-button navigation bar', () {
|
||||||
|
// 3-button navigation typical height is ~48dp
|
||||||
|
const systemNavBarHeight = 48.0;
|
||||||
|
const keyboardHeight = 0.0;
|
||||||
|
const extraPadding = 20.0;
|
||||||
|
|
||||||
|
final totalBottomPadding = systemNavBarHeight + keyboardHeight + extraPadding;
|
||||||
|
expect(totalBottomPadding, 68.0);
|
||||||
|
// Ensures interactive action buttons sit strictly above the navigation bar
|
||||||
|
expect(totalBottomPadding > systemNavBarHeight, isTrue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Post Edit & Permissions Tests', () {
|
||||||
|
test('PostItem.copyWith updates fields while preserving existing values', () {
|
||||||
|
final original = PostItem(
|
||||||
|
id: 'post-1',
|
||||||
|
title: 'Original Title',
|
||||||
|
description: 'Original Desc',
|
||||||
|
createdDate: DateTime(2026, 1, 1),
|
||||||
|
authorUid: 'user-123',
|
||||||
|
imageUrl: 'https://example.com/img1.jpg',
|
||||||
|
street: 'Hauptstraße',
|
||||||
|
category: 'Sonstiges',
|
||||||
|
status: 'Gemeldet',
|
||||||
|
);
|
||||||
|
|
||||||
|
final edited = original.copyWith(
|
||||||
|
title: 'Updated Title',
|
||||||
|
description: 'Updated Desc',
|
||||||
|
imageUrl: 'https://example.com/img2.jpg',
|
||||||
|
imageUrls: ['https://example.com/img2.jpg'],
|
||||||
|
status: 'In Bearbeitung',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(edited.id, 'post-1');
|
||||||
|
expect(edited.authorUid, 'user-123');
|
||||||
|
expect(edited.createdDate, DateTime(2026, 1, 1));
|
||||||
|
expect(edited.street, 'Hauptstraße');
|
||||||
|
expect(edited.category, 'Sonstiges');
|
||||||
|
expect(edited.title, 'Updated Title');
|
||||||
|
expect(edited.description, 'Updated Desc');
|
||||||
|
expect(edited.imageUrl, 'https://example.com/img2.jpg');
|
||||||
|
expect(edited.imageUrls, ['https://example.com/img2.jpg']);
|
||||||
|
expect(edited.status, 'In Bearbeitung');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('verifies post edit permissions: admins can edit all, users can edit only their own, Aussendungen cannot be edited', () {
|
||||||
|
bool canEditPost({
|
||||||
|
required PostItem post,
|
||||||
|
required String? currentUserId,
|
||||||
|
required bool isAdmin,
|
||||||
|
}) {
|
||||||
|
if (post.isAussendung) return false;
|
||||||
|
final isAuthor = currentUserId != null && post.authorUid == currentUserId;
|
||||||
|
return isAuthor || isAdmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
final userPost = PostItem(
|
||||||
|
id: 'user_post_1',
|
||||||
|
title: 'Schlagloch',
|
||||||
|
description: 'Großes Schlagloch',
|
||||||
|
createdDate: DateTime.now(),
|
||||||
|
authorUid: 'author_1',
|
||||||
|
);
|
||||||
|
|
||||||
|
final aussendung = PostItem(
|
||||||
|
id: 'wp_123',
|
||||||
|
title: 'Aussendung',
|
||||||
|
description: 'PDF',
|
||||||
|
createdDate: DateTime.now(),
|
||||||
|
isAussendung: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 1. Author user can edit their own post
|
||||||
|
expect(
|
||||||
|
canEditPost(post: userPost, currentUserId: 'author_1', isAdmin: false),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Different user cannot edit other user's post
|
||||||
|
expect(
|
||||||
|
canEditPost(post: userPost, currentUserId: 'other_user', isAdmin: false),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Admin can edit any post (even if not author)
|
||||||
|
expect(
|
||||||
|
canEditPost(post: userPost, currentUserId: 'other_user', isAdmin: true),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Admin cannot edit external WordPress Aussendungen
|
||||||
|
expect(
|
||||||
|
canEditPost(post: aussendung, currentUserId: 'author_1', isAdmin: true),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5. Unauthenticated user cannot edit
|
||||||
|
expect(
|
||||||
|
canEditPost(post: userPost, currentUserId: null, isAdmin: false),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('validates issue status editing for admins', () {
|
||||||
|
const allowedStatuses = ['Gemeldet', 'In Bearbeitung', 'Erledigt'];
|
||||||
|
|
||||||
|
final issue = PostItem(
|
||||||
|
id: 'issue_1',
|
||||||
|
title: 'Straßenlaterne',
|
||||||
|
description: 'Laterne dunkel',
|
||||||
|
createdDate: DateTime.now(),
|
||||||
|
authorUid: 'user_1',
|
||||||
|
status: 'Gemeldet',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(issue.status, 'Gemeldet');
|
||||||
|
|
||||||
|
// Admin transitions status
|
||||||
|
final inProgress = issue.copyWith(status: 'In Bearbeitung');
|
||||||
|
expect(inProgress.status, 'In Bearbeitung');
|
||||||
|
expect(allowedStatuses.contains(inProgress.status), isTrue);
|
||||||
|
|
||||||
|
final done = inProgress.copyWith(status: 'Erledigt');
|
||||||
|
expect(done.status, 'Erledigt');
|
||||||
|
expect(allowedStatuses.contains(done.status), isTrue);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user