✨ File management actions
This commit is contained in:
@@ -13,6 +13,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:island/widgets/sites/info_row.dart';
|
||||
import 'package:island/widgets/sites/pages_section.dart';
|
||||
import 'package:island/widgets/sites/file_management_section.dart';
|
||||
import 'package:island/widgets/sites/file_management_action_section.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/services/time.dart';
|
||||
import 'package:island/widgets/extended_refresh_indicator.dart';
|
||||
@@ -94,7 +95,10 @@ class PublicationSiteDetailScreen extends HookConsumerWidget {
|
||||
flex: 2,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Card(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
@@ -102,9 +106,8 @@ class PublicationSiteDetailScreen extends HookConsumerWidget {
|
||||
children: [
|
||||
Text(
|
||||
'Site Information',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
style: theme.textTheme.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const Gap(16),
|
||||
InfoRow(
|
||||
@@ -165,6 +168,14 @@ class PublicationSiteDetailScreen extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
if (site.mode == 1) // Self-Managed only
|
||||
FileManagementActionSection(
|
||||
site: site,
|
||||
pubName: pubName,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
160
lib/widgets/sites/file_management_action_section.dart
Normal file
160
lib/widgets/sites/file_management_action_section.dart
Normal file
@@ -0,0 +1,160 @@
|
||||
import 'dart:io';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:island/models/publication_site.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/pods/site_files.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class FileManagementActionSection extends HookConsumerWidget {
|
||||
final SnPublicationSite site;
|
||||
final String pubName;
|
||||
|
||||
const FileManagementActionSection({
|
||||
super.key,
|
||||
required this.site,
|
||||
required this.pubName,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Card(
|
||||
child: Column(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'File Actions',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
).padding(horizontal: 16, top: 16),
|
||||
Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
Symbols.delete_forever,
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
title: const Text('Purge Files'),
|
||||
subtitle: const Text(
|
||||
'Remove all uploaded files from the site',
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
onTap: () => _purgeFiles(context, ref),
|
||||
),
|
||||
const Gap(8),
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
Symbols.upload,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
title: const Text('Deploy Site'),
|
||||
subtitle: const Text(
|
||||
'Upload and deploy a new version from ZIP archive',
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
onTap: () => _deploySite(context, ref),
|
||||
),
|
||||
],
|
||||
).padding(vertical: 8),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _purgeFiles(BuildContext context, WidgetRef ref) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: const Text('Confirm Purge'),
|
||||
content: const Text(
|
||||
'This will permanently delete all files uploaded to this site. This action cannot be undone. Are you sure you want to continue?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
child: const Text('Purge All Files'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed != true) return;
|
||||
|
||||
try {
|
||||
final apiClient = ref.read(apiClientProvider);
|
||||
await apiClient.delete('/zone/sites/${site.id}/files/purge');
|
||||
if (context.mounted) {
|
||||
showSnackBar('All files purged successfully');
|
||||
// Refresh the file management section
|
||||
ref.invalidate(siteFilesProvider(siteId: site.id));
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
showSnackBar('Failed to purge files: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deploySite(BuildContext context, WidgetRef ref) async {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['zip'],
|
||||
allowMultiple: false,
|
||||
);
|
||||
|
||||
if (result == null || result.files.isEmpty) {
|
||||
return; // User canceled
|
||||
}
|
||||
|
||||
final file = File(result.files.first.path!);
|
||||
|
||||
try {
|
||||
final apiClient = ref.read(apiClientProvider);
|
||||
|
||||
// Create multipart form data
|
||||
final formData = FormData.fromMap({
|
||||
'file': await MultipartFile.fromFile(
|
||||
file.path,
|
||||
filename: result.files.first.name,
|
||||
contentType: MediaType('application', 'zip'),
|
||||
),
|
||||
});
|
||||
|
||||
await apiClient.post(
|
||||
'/zone/sites/${site.id}/files/deploy',
|
||||
data: formData,
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
showSnackBar('Site deployed successfully');
|
||||
// Refresh the file management section
|
||||
ref.invalidate(siteFilesProvider(siteId: site.id));
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
showSnackBar('Failed to deploy site: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/publication_site.dart';
|
||||
import 'package:island/widgets/sites/file_management_section.dart';
|
||||
import 'package:island/widgets/sites/file_management_action_section.dart';
|
||||
import 'package:island/widgets/sites/info_row.dart';
|
||||
import 'package:island/widgets/sites/pages_section.dart';
|
||||
import 'package:island/services/time.dart';
|
||||
@@ -90,6 +91,9 @@ class SiteDetailContent extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
if (site.mode == 1) // Self-Managed only
|
||||
FileManagementActionSection(site: site, pubName: pubName),
|
||||
// Pages Section
|
||||
PagesSection(site: site, pubName: pubName),
|
||||
FileManagementSection(site: site, pubName: pubName),
|
||||
|
||||
Reference in New Issue
Block a user