web deploy
This commit is contained in:
@@ -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 (confirm != true) return;
|
if (!context.mounted) return;
|
||||||
final messenger = ScaffoldMessenger.of(context);
|
if (confirm != true) return;
|
||||||
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,10 +1411,11 @@ extension _SeriesContextMenu on SeriesListScreen {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (confirm != true) return;
|
if (!context.mounted) return;
|
||||||
final messenger = ScaffoldMessenger.of(context);
|
if (confirm != true) return;
|
||||||
try {
|
final messenger = ScaffoldMessenger.of(context);
|
||||||
final backend = ref.read(backendApiProvider);
|
try {
|
||||||
|
final backend = ref.read(backendApiProvider);
|
||||||
await backend.deleteShow(sid);
|
await backend.deleteShow(sid);
|
||||||
// ignore: unused_result
|
// ignore: unused_result
|
||||||
ref.invalidate(seriesGroupedProvider);
|
ref.invalidate(seriesGroupedProvider);
|
||||||
@@ -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>
|
||||||
|
|
||||||
Reference in New Issue
Block a user