web deploy

main
Herwig Birke 2 months ago
parent 2cf1fb648c
commit f7839d1dfb

@ -0,0 +1,49 @@
name: Deploy Flutter Web to GitHub Pages
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: write
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
- name: Flutter version
run: flutter --version
- name: Enable web and get packages
run: |
flutter config --enable-web
flutter pub get
- name: Build Flutter web (release)
env:
REPO_NAME: ${{ github.event.repository.name }}
run: |
# Use canvaskit for better desktop rendering; adjust if needed
flutter build web \
--release \
--web-renderer canvaskit \
--base-href "/${REPO_NAME}/"
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./build/web
# Keep history small
force_orphan: true

@ -127,7 +127,34 @@ class _MovieDetailScreenState extends ConsumerState<MovieDetailScreen> {
label: const Text('Speichern'), label: const Text('Speichern'),
) )
: null, : null,
body: SizedBox.expand( body: MenuAnchor(
menuChildren: [
MenuItemButton(
child: const Text('update'),
onPressed: () async {
final messenger = ScaffoldMessenger.of(context);
try {
final tmdb = ref.read(tmdbApiProvider);
final json = await tmdb.getMovie(m.tmdbId);
await ref.read(backendApiProvider).upsertMovie(json);
if (!mounted) return;
setState(() => _tmdb = json);
ref.invalidate(moviesStreamProvider);
ref.invalidate(moviesProvider);
messenger.showSnackBar(const SnackBar(content: Text('TMDB Daten aktualisiert')));
} catch (e) {
messenger.showSnackBar(SnackBar(content: Text('TMDB Update fehlgeschlagen: $e')));
}
},
),
],
builder: (ctx, controller, child) => GestureDetector(
onSecondaryTapDown: (_) => controller.open(),
onSecondaryTapUp: (_) => controller.open(),
onLongPress: () => controller.open(),
child: child!,
),
child: SizedBox.expand(
child: Container( child: Container(
decoration: (backdrop != null || poster != null) decoration: (backdrop != null || poster != null)
? BoxDecoration( ? BoxDecoration(
@ -248,7 +275,7 @@ class _MovieDetailScreenState extends ConsumerState<MovieDetailScreen> {
), ),
), ),
), ),
); ));
} }
Widget _statusSelector(ItemStatus current) { Widget _statusSelector(ItemStatus current) {

@ -215,6 +215,7 @@ class MovieListScreen extends ConsumerWidget {
} }
Future<void> deleteMovie() async { Future<void> deleteMovie() async {
final messenger = ScaffoldMessenger.of(context);
final confirm = await showDialog<bool>( final confirm = await showDialog<bool>(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
@ -226,8 +227,8 @@ class MovieListScreen extends ConsumerWidget {
], ],
), ),
); );
if (!context.mounted) return;
if (confirm != true) return; if (confirm != true) return;
final messenger = ScaffoldMessenger.of(context);
try { try {
await ref.read(backendApiProvider).deleteMovie(m.id); await ref.read(backendApiProvider).deleteMovie(m.id);
ref.invalidate(moviesStreamProvider); ref.invalidate(moviesStreamProvider);

@ -2,7 +2,6 @@ import 'dart:convert';
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:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart' as widgets; import 'package:flutter/widgets.dart' as widgets;
import '../../../core/status.dart'; import '../../../core/status.dart';
import '../../movies/presentation/widgets/status_chip.dart'; import '../../movies/presentation/widgets/status_chip.dart';
@ -504,6 +503,7 @@ class SeriesListScreen extends ConsumerWidget {
onPressed: () async { onPressed: () async {
final sid = int.tryParse(showKey.split('##').last); final sid = int.tryParse(showKey.split('##').last);
if (sid == null) return; if (sid == null) return;
final messenger = ScaffoldMessenger.of(context);
final confirm = await showDialog<bool>( final confirm = await showDialog<bool>(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
@ -515,8 +515,8 @@ class SeriesListScreen extends ConsumerWidget {
], ],
), ),
); );
if (!context.mounted) return;
if (confirm != true) return; if (confirm != true) return;
final messenger = ScaffoldMessenger.of(context);
try { try {
final backend = ref.read(backendApiProvider); final backend = ref.read(backendApiProvider);
await backend.deleteShow(sid); await backend.deleteShow(sid);
@ -650,6 +650,7 @@ class SeriesListScreen extends ConsumerWidget {
onPressed: () async { onPressed: () async {
final sid = int.tryParse(showKey.split('##').last); final sid = int.tryParse(showKey.split('##').last);
if (sid == null) return; if (sid == null) return;
final messenger = ScaffoldMessenger.of(context);
final confirm = await showDialog<bool>( final confirm = await showDialog<bool>(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
@ -661,8 +662,8 @@ class SeriesListScreen extends ConsumerWidget {
], ],
), ),
); );
if (!context.mounted) return;
if (confirm != true) return; if (confirm != true) return;
final messenger = ScaffoldMessenger.of(context);
try { final backend = ref.read(backendApiProvider); await backend.deleteShow(sid); ref.invalidate(seriesGroupedProvider); ref.invalidate(seriesSummaryProvider); messenger.showSnackBar(const SnackBar(content: Text('Serie gelöscht')));} catch (e) { messenger.showSnackBar(SnackBar(content: Text('Löschen fehlgeschlagen: $e')));} try { final backend = ref.read(backendApiProvider); await backend.deleteShow(sid); ref.invalidate(seriesGroupedProvider); ref.invalidate(seriesSummaryProvider); messenger.showSnackBar(const SnackBar(content: Text('Serie gelöscht')));} catch (e) { messenger.showSnackBar(SnackBar(content: Text('Löschen fehlgeschlagen: $e')));}
}, },
), ),
@ -724,7 +725,7 @@ class SeriesListScreen extends ConsumerWidget {
MenuItemButton( MenuItemButton(
child: const Text('delete'), child: const Text('delete'),
onPressed: () async { onPressed: () async {
final sid = int.tryParse(showKey.split('##').last); if (sid == null) return; final confirm = await showDialog<bool>(context: context, builder: (ctx) => AlertDialog(title: const Text('Serie löschen?'), content: Text(displayName), actions: [TextButton(onPressed: () => Navigator.of(ctx).pop(false), child: const Text('Abbrechen')), FilledButton(onPressed: () => Navigator.of(ctx).pop(true), child: const Text('Löschen'))])); if (confirm != true) return; final messenger = ScaffoldMessenger.of(context); try { final backend = ref.read(backendApiProvider); await backend.deleteShow(sid); ref.invalidate(seriesGroupedProvider); ref.invalidate(seriesSummaryProvider); messenger.showSnackBar(const SnackBar(content: Text('Serie gelöscht')));} catch (e) { messenger.showSnackBar(SnackBar(content: Text('Löschen fehlgeschlagen: $e')));} final sid = int.tryParse(showKey.split('##').last); if (sid == null) return; final confirm = await showDialog<bool>(context: context, builder: (ctx) => AlertDialog(title: const Text('Serie löschen?'), content: Text(displayName), actions: [TextButton(onPressed: () => Navigator.of(ctx).pop(false), child: const Text('Abbrechen')), FilledButton(onPressed: () => Navigator.of(ctx).pop(true), child: const Text('Löschen'))])); if (!context.mounted) return; if (confirm != true) return; final messenger = ScaffoldMessenger.of(context); try { final backend = ref.read(backendApiProvider); await backend.deleteShow(sid); ref.invalidate(seriesGroupedProvider); ref.invalidate(seriesSummaryProvider); messenger.showSnackBar(const SnackBar(content: Text('Serie gelöscht')));} catch (e) { messenger.showSnackBar(SnackBar(content: Text('Löschen fehlgeschlagen: $e')));}
}, },
), ),
MenuItemButton( MenuItemButton(
@ -1410,6 +1411,7 @@ extension _SeriesContextMenu on SeriesListScreen {
], ],
), ),
); );
if (!context.mounted) return;
if (confirm != true) return; if (confirm != true) return;
final messenger = ScaffoldMessenger.of(context); final messenger = ScaffoldMessenger.of(context);
try { try {
@ -1439,12 +1441,17 @@ extension _SeriesContextMenu on SeriesListScreen {
} }
return [ return [
MenuItemButton(child: const Text('set all progress to done'), onPressed: setAllProgressToDone), MenuItemButton(onPressed: setAllProgressToDone, child: const Text('set all progress to done')),
MenuItemButton(child: const Text('update'), onPressed: performUpdate), MenuItemButton(onPressed: performUpdate, child: const Text('update')),
MenuItemButton(child: const Text('delete'), onPressed: performDelete), MenuItemButton(onPressed: performDelete, child: const Text('delete')),
MenuItemButton(child: const Text('open download path'), onPressed: openDownload), MenuItemButton(onPressed: openDownload, child: const Text('open download path')),
]; ];
} }
} }

@ -0,0 +1,53 @@
# Flutter Web on Apache: SPA routing, caching, compression, and correct MIME types
<IfModule mod_rewrite.c>
Options -MultiViews
RewriteEngine On
# Serve existing files/directories directly
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
# Fallback all other requests to index.html (client-side routing)
RewriteRule ^ index.html [L]
</IfModule>
# Correct MIME types (important for CanvasKit/WebAssembly and fonts)
AddType application/wasm wasm
AddType font/woff2 woff2
AddType application/javascript js
AddType text/css css
# Caching policy
<IfModule mod_headers.c>
# Long cache for static assets (Flutter uses hashed filenames)
<FilesMatch "\.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|wasm)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
# Service configs/JSON can be cached briefly
<FilesMatch "\.(json)$">
Header set Cache-Control "public, max-age=3600"
</FilesMatch>
# Never cache the HTML entry point
<FilesMatch "^index\.html$">
Header set Cache-Control "no-cache, no-store, must-revalidate, max-age=0"
</FilesMatch>
# Minimal hardening
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>
# Compression (enable if modules are available)
<IfModule mod_brotli.c>
AddOutputFilterByType BROTLI_COMPRESS \
text/html text/plain text/css application/javascript application/json application/wasm image/svg+xml
</IfModule>
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE \
text/html text/plain text/css application/javascript application/json application/wasm image/svg+xml
</IfModule>
Loading…
Cancel
Save