💄 Optimize publication site screen
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user