File management actions

This commit is contained in:
2025-11-22 16:01:27 +08:00
parent 3061f0c5a9
commit f2c1b2a531
3 changed files with 243 additions and 68 deletions

View File

@@ -13,6 +13,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:island/widgets/sites/info_row.dart'; import 'package:island/widgets/sites/info_row.dart';
import 'package:island/widgets/sites/pages_section.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_section.dart';
import 'package:island/widgets/sites/file_management_action_section.dart';
import 'package:island/services/responsive.dart'; import 'package:island/services/responsive.dart';
import 'package:island/services/time.dart'; import 'package:island/services/time.dart';
import 'package:island/widgets/extended_refresh_indicator.dart'; import 'package:island/widgets/extended_refresh_indicator.dart';
@@ -94,76 +95,86 @@ class PublicationSiteDetailScreen extends HookConsumerWidget {
flex: 2, flex: 2,
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
child: Card( child: Column(
child: Padding( crossAxisAlignment: CrossAxisAlignment.start,
padding: const EdgeInsets.all(16), children: [
child: Column( Card(
crossAxisAlignment: CrossAxisAlignment.start, child: Padding(
children: [ padding: const EdgeInsets.all(16),
Text( child: Column(
'Site Information', crossAxisAlignment: CrossAxisAlignment.start,
style: theme.textTheme.titleMedium?.copyWith( children: [
fontWeight: FontWeight.bold, Text(
), 'Site Information',
style: theme.textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.bold),
),
const Gap(16),
InfoRow(
label: 'Name',
value: site.name,
icon: Symbols.title,
),
const Gap(8),
InfoRow(
label: 'Slug',
value: site.slug,
icon: Symbols.tag,
monospace: true,
),
const Gap(8),
InfoRow(
label: 'Domain',
value: '${site.slug}.solian.page',
icon: Symbols.globe,
monospace: true,
onTap: () {
final url =
'https://${site.slug}.solian.page';
launchUrlString(url);
},
),
const Gap(8),
InfoRow(
label: 'Mode',
value:
site.mode == 0
? 'Fully Managed'
: 'Self-Managed',
icon: Symbols.settings,
),
if (site.description != null &&
site.description!.isNotEmpty) ...[
const Gap(8),
InfoRow(
label: 'Description',
value: site.description!,
icon: Symbols.description,
),
],
const Gap(8),
InfoRow(
label: 'Created',
value: site.createdAt.formatSystem(),
icon: Symbols.calendar_add_on,
),
const Gap(8),
InfoRow(
label: 'Updated',
value: site.updatedAt.formatSystem(),
icon: Symbols.update,
),
],
), ),
const Gap(16), ),
InfoRow(
label: 'Name',
value: site.name,
icon: Symbols.title,
),
const Gap(8),
InfoRow(
label: 'Slug',
value: site.slug,
icon: Symbols.tag,
monospace: true,
),
const Gap(8),
InfoRow(
label: 'Domain',
value: '${site.slug}.solian.page',
icon: Symbols.globe,
monospace: true,
onTap: () {
final url =
'https://${site.slug}.solian.page';
launchUrlString(url);
},
),
const Gap(8),
InfoRow(
label: 'Mode',
value:
site.mode == 0
? 'Fully Managed'
: 'Self-Managed',
icon: Symbols.settings,
),
if (site.description != null &&
site.description!.isNotEmpty) ...[
const Gap(8),
InfoRow(
label: 'Description',
value: site.description!,
icon: Symbols.description,
),
],
const Gap(8),
InfoRow(
label: 'Created',
value: site.createdAt.formatSystem(),
icon: Symbols.calendar_add_on,
),
const Gap(8),
InfoRow(
label: 'Updated',
value: site.updatedAt.formatSystem(),
icon: Symbols.update,
),
],
), ),
), const Gap(8),
if (site.mode == 1) // Self-Managed only
FileManagementActionSection(
site: site,
pubName: pubName,
),
],
), ),
), ),
), ),

View 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');
}
}
}
}

View File

@@ -3,6 +3,7 @@ import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/publication_site.dart'; import 'package:island/models/publication_site.dart';
import 'package:island/widgets/sites/file_management_section.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/info_row.dart';
import 'package:island/widgets/sites/pages_section.dart'; import 'package:island/widgets/sites/pages_section.dart';
import 'package:island/services/time.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 // Pages Section
PagesSection(site: site, pubName: pubName), PagesSection(site: site, pubName: pubName),
FileManagementSection(site: site, pubName: pubName), FileManagementSection(site: site, pubName: pubName),