💄 Optimize publication site screen

This commit is contained in:
2025-11-22 14:39:03 +08:00
parent 4fb739b33b
commit 9b85b7573c
4 changed files with 345 additions and 336 deletions

View File

@@ -17,6 +17,7 @@ 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';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:url_launcher/url_launcher_string.dart';
part 'site_detail.g.dart'; part 'site_detail.g.dart';
@@ -83,6 +84,7 @@ class PublicationSiteDetailScreen extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
PagesSection(site: site, pubName: pubName), PagesSection(site: site, pubName: pubName),
if (site.mode == 1) // Self-Managed only
FileManagementSection(site: site, pubName: pubName), FileManagementSection(site: site, pubName: pubName),
], ],
), ),
@@ -118,6 +120,18 @@ class PublicationSiteDetailScreen extends HookConsumerWidget {
monospace: true, monospace: true,
), ),
const Gap(8), 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( InfoRow(
label: 'Mode', label: 'Mode',
value: value:

View File

@@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.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/pods/network.dart'; import 'package:island/pods/network.dart';
@@ -81,6 +82,7 @@ class CreatorSiteListScreen extends HookConsumerWidget {
onRefresh: () => ref.refresh(siteListNotifierProvider(pubName).future), onRefresh: () => ref.refresh(siteListNotifierProvider(pubName).future),
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
const SliverGap(8),
PagingHelperSliverView( PagingHelperSliverView(
provider: siteListNotifierProvider(pubName), provider: siteListNotifierProvider(pubName),
futureRefreshable: siteListNotifierProvider(pubName).future, futureRefreshable: siteListNotifierProvider(pubName).future,
@@ -115,35 +117,54 @@ class _CreatorSiteItem extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
return Card( return Card(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: ListTile( child: InkWell(
title: Text(site.name), onTap: () {
subtitle: Column( // Navigate to site detail screen
context.pushNamed(
'creatorSiteDetail',
pathParameters: {'name': pubName, 'siteSlug': site.slug},
);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (site.description != null && site.description!.isNotEmpty) Expanded(
Padding( child: Column(
padding: const EdgeInsets.only(top: 4), spacing: 2,
child: Text( crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Symbols.globe,
size: 18,
color: Theme.of(context).colorScheme.primary,
),
const Gap(6),
Text(site.name).bold(),
],
),
if (site.description != null &&
site.description!.isNotEmpty)
Text(
site.description!, site.description!,
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), const Divider(height: 8),
Padding( Text(
padding: const EdgeInsets.only(top: 4), '${site.slug}.solian.page',
child: Text( style: GoogleFonts.robotoMono(fontSize: 11),
'Slug: ${site.slug} · Pages: ${site.pages.length}', ).opacity(0.8),
style: theme.textTheme.bodySmall,
),
),
], ],
), ),
trailing: PopupMenuButton<String>( ),
PopupMenuButton<String>(
itemBuilder: itemBuilder:
(context) => [ (context) => [
PopupMenuItem( PopupMenuItem(
@@ -159,8 +180,10 @@ class _CreatorSiteItem extends HookConsumerWidget {
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder:
(context) => (context) => SiteForm(
SiteForm(pubName: pubName, siteSlug: site.slug), pubName: pubName,
siteSlug: site.slug,
),
); );
}, },
), ),
@@ -184,7 +207,8 @@ class _CreatorSiteItem extends HookConsumerWidget {
actions: [ actions: [
TextButton( TextButton(
onPressed: onPressed:
() => Navigator.of(context).pop(false), () =>
Navigator.of(context).pop(false),
child: Text('Cancel'), child: Text('Cancel'),
), ),
TextButton( TextButton(
@@ -210,7 +234,9 @@ class _CreatorSiteItem extends HookConsumerWidget {
} catch (e) { } catch (e) {
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to delete site')), SnackBar(
content: Text('Failed to delete site'),
),
); );
} }
} }
@@ -219,13 +245,9 @@ class _CreatorSiteItem extends HookConsumerWidget {
), ),
], ],
), ),
onTap: () { ],
// Navigate to site detail screen ),
context.pushNamed( ),
'creatorSiteDetail',
pathParameters: {'name': pubName, 'siteSlug': site.slug},
);
},
), ),
); );
} }

View File

@@ -7,6 +7,7 @@ class InfoRow extends StatelessWidget {
final String value; final String value;
final IconData icon; final IconData icon;
final bool monospace; final bool monospace;
final VoidCallback? onTap;
const InfoRow({ const InfoRow({
super.key, super.key,
@@ -14,10 +15,22 @@ class InfoRow extends StatelessWidget {
required this.value, required this.value,
required this.icon, required this.icon,
this.monospace = false, this.monospace = false,
this.onTap,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget valueWidget = Text(
value,
style:
monospace
? GoogleFonts.robotoMono(fontSize: 14)
: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.end,
);
if (onTap != null) valueWidget = InkWell(onTap: onTap, child: valueWidget);
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -33,17 +46,7 @@ class InfoRow extends StatelessWidget {
), ),
), ),
const Gap(12), const Gap(12),
Expanded( Expanded(flex: 3, child: valueWidget),
flex: 3,
child: Text(
value,
style:
monospace
? GoogleFonts.robotoMono(fontSize: 14)
: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.end,
),
),
], ],
); );
} }

View File

@@ -203,9 +203,7 @@ class PageForm extends HookConsumerWidget {
return SheetScaffold( return SheetScaffold(
titleText: page == null ? 'Create Page' : 'Edit Page', titleText: page == null ? 'Create Page' : 'Edit Page',
child: Builder( child: SingleChildScrollView(
builder:
(context) => SingleChildScrollView(
child: Column( child: Column(
children: [ children: [
Form( Form(
@@ -218,9 +216,7 @@ class PageForm extends HookConsumerWidget {
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Page Type', labelText: 'Page Type',
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.all( borderRadius: BorderRadius.all(Radius.circular(12)),
Radius.circular(12),
),
), ),
), ),
items: const [ items: const [
@@ -266,18 +262,14 @@ class PageForm extends HookConsumerWidget {
labelText: 'Page Path', labelText: 'Page Path',
hintText: '/about, /contact, etc.', hintText: '/about, /contact, etc.',
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.all( borderRadius: BorderRadius.all(Radius.circular(12)),
Radius.circular(12),
),
), ),
), ),
validator: (value) { validator: (value) {
if (value == null || value.isEmpty) { if (value == null || value.isEmpty) {
return 'Please enter a page path'; return 'Please enter a page path';
} }
if (!RegExp( if (!RegExp(r'^[a-zA-Z0-9\-/_]+$').hasMatch(value)) {
r'^[a-zA-Z0-9\-/_]+$',
).hasMatch(value)) {
return 'Page path can only contain letters, numbers, hyphens, underscores, and slashes'; return 'Page path can only contain letters, numbers, hyphens, underscores, and slashes';
} }
if (!value.startsWith('/')) { if (!value.startsWith('/')) {
@@ -289,9 +281,7 @@ class PageForm extends HookConsumerWidget {
return null; return null;
}, },
onTapOutside: onTapOutside:
(_) => (_) => FocusManager.instance.primaryFocus?.unfocus(),
FocusManager.instance.primaryFocus
?.unfocus(),
).padding(horizontal: 20), ).padding(horizontal: 20),
const SizedBox(height: 16), const SizedBox(height: 16),
TextFormField( TextFormField(
@@ -300,9 +290,7 @@ class PageForm extends HookConsumerWidget {
labelText: 'Page Title', labelText: 'Page Title',
hintText: 'About Us, Contact, etc.', hintText: 'About Us, Contact, etc.',
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.all( borderRadius: BorderRadius.all(Radius.circular(12)),
Radius.circular(12),
),
), ),
), ),
validator: (value) { validator: (value) {
@@ -312,9 +300,7 @@ class PageForm extends HookConsumerWidget {
return null; return null;
}, },
onTapOutside: onTapOutside:
(_) => (_) => FocusManager.instance.primaryFocus?.unfocus(),
FocusManager.instance.primaryFocus
?.unfocus(),
).padding(horizontal: 20), ).padding(horizontal: 20),
const SizedBox(height: 16), const SizedBox(height: 16),
TextFormField( TextFormField(
@@ -324,17 +310,13 @@ class PageForm extends HookConsumerWidget {
hintText: hintText:
'<h1>Hello World</h1><p>This is my page content...</p>', '<h1>Hello World</h1><p>This is my page content...</p>',
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.all( borderRadius: BorderRadius.all(Radius.circular(12)),
Radius.circular(12),
),
), ),
alignLabelWithHint: true, alignLabelWithHint: true,
), ),
maxLines: 10, maxLines: 10,
onTapOutside: onTapOutside:
(_) => (_) => FocusManager.instance.primaryFocus?.unfocus(),
FocusManager.instance.primaryFocus
?.unfocus(),
validator: (value) { validator: (value) {
if (value == null || value.isEmpty) { if (value == null || value.isEmpty) {
return 'Please enter HTML content for the page'; return 'Please enter HTML content for the page';
@@ -350,19 +332,14 @@ class PageForm extends HookConsumerWidget {
labelText: 'Page Path', labelText: 'Page Path',
hintText: '/old-page, /redirect, etc.', hintText: '/old-page, /redirect, etc.',
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.all( borderRadius: BorderRadius.all(Radius.circular(12)),
Radius.circular(12),
), ),
), ),
prefixText: '/',
),
validator: (value) { validator: (value) {
if (value == null || value.isEmpty) { if (value == null || value.isEmpty) {
return 'Please enter a page path'; return 'Please enter a page path';
} }
if (!RegExp( if (!RegExp(r'^[a-zA-Z0-9\-/_]+$').hasMatch(value)) {
r'^[a-zA-Z0-9\-/_]+$',
).hasMatch(value)) {
return 'Page path can only contain letters, numbers, hyphens, underscores, and slashes'; return 'Page path can only contain letters, numbers, hyphens, underscores, and slashes';
} }
if (!value.startsWith('/')) { if (!value.startsWith('/')) {
@@ -374,9 +351,7 @@ class PageForm extends HookConsumerWidget {
return null; return null;
}, },
onTapOutside: onTapOutside:
(_) => (_) => FocusManager.instance.primaryFocus?.unfocus(),
FocusManager.instance.primaryFocus
?.unfocus(),
).padding(horizontal: 20), ).padding(horizontal: 20),
const SizedBox(height: 16), const SizedBox(height: 16),
TextFormField( TextFormField(
@@ -385,9 +360,7 @@ class PageForm extends HookConsumerWidget {
labelText: 'Redirect Target', labelText: 'Redirect Target',
hintText: '/new-page, https://example.com, etc.', hintText: '/new-page, https://example.com, etc.',
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.all( borderRadius: BorderRadius.all(Radius.circular(12)),
Radius.circular(12),
),
), ),
), ),
validator: (value) { validator: (value) {
@@ -402,14 +375,8 @@ class PageForm extends HookConsumerWidget {
return null; return null;
}, },
onTapOutside: onTapOutside:
(_) => (_) => FocusManager.instance.primaryFocus?.unfocus(),
FocusManager.instance.primaryFocus
?.unfocus(),
).padding(horizontal: 20), ).padding(horizontal: 20),
],
],
).padding(vertical: 20),
),
Row( Row(
children: [ children: [
if (page != null) ...[ if (page != null) ...[
@@ -430,10 +397,13 @@ class PageForm extends HookConsumerWidget {
label: const Text('Save Page'), label: const Text('Save Page'),
), ),
], ],
).padding(horizontal: 20, vertical: 12), ).padding(horizontal: 20, vertical: 16),
],
], ],
), ),
), ),
],
),
), ),
); );
} }