709 lines
20 KiB
Dart
Raw Normal View History

import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
2024-10-31 14:38:32 +08:00
import 'package:appflowy/plugins/shared/share/constants.dart';
import 'package:appflowy/plugins/shared/share/publish_color_extension.dart';
import 'package:appflowy/plugins/shared/share/publish_name_generator.dart';
import 'package:appflowy/plugins/shared/share/share_bloc.dart';
2024-10-31 14:38:32 +08:00
import 'package:appflowy/shared/error_code/error_code_map.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/string_extension.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/widget/rounded_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class PublishTab extends StatelessWidget {
const PublishTab({
super.key,
required this.viewName,
});
final String viewName;
@override
Widget build(BuildContext context) {
return BlocConsumer<ShareBloc, ShareState>(
listener: (context, state) {
_showToast(context, state);
},
builder: (context, state) {
if (state.isPublished) {
return _PublishedWidget(
url: state.url,
2024-10-31 14:38:32 +08:00
pathName: state.pathName,
namespace: state.namespace,
onVisitSite: (url) => afLaunchUrlString(url),
onUnPublish: () {
context.read<ShareBloc>().add(const ShareEvent.unPublish());
},
);
} else {
return _PublishWidget(
onPublish: (selectedViews) async {
final id = context.read<ShareBloc>().view.id;
final lastPublishName = context.read<ShareBloc>().state.pathName;
final publishName = lastPublishName.orDefault(
await generatePublishName(
id,
viewName,
),
);
if (selectedViews.isNotEmpty) {
Log.info(
'Publishing views: ${selectedViews.map((e) => e.name)}',
);
}
if (context.mounted) {
context.read<ShareBloc>().add(
ShareEvent.publish(
'',
publishName,
selectedViews.map((e) => e.id).toList(),
),
);
}
},
);
}
},
);
}
void _showToast(BuildContext context, ShareState state) {
if (state.publishResult != null) {
state.publishResult!.fold(
(value) => showToastNotification(
context,
message: LocaleKeys.publish_publishSuccessfully.tr(),
),
(error) => showToastNotification(
context,
message: '${LocaleKeys.publish_publishFailed.tr()}: ${error.code}',
2024-10-31 14:38:32 +08:00
type: ToastificationType.error,
),
);
} else if (state.unpublishResult != null) {
state.unpublishResult!.fold(
(value) => showToastNotification(
context,
message: LocaleKeys.publish_unpublishSuccessfully.tr(),
),
(error) => showToastNotification(
context,
message: LocaleKeys.publish_unpublishFailed.tr(),
description: error.msg,
2024-10-31 14:38:32 +08:00
type: ToastificationType.error,
),
);
2024-10-31 14:38:32 +08:00
} else if (state.updatePathNameResult != null) {
state.updatePathNameResult!.fold(
(value) => showToastNotification(
context,
message: LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(),
),
(error) {
Log.error('update path name failed: $error');
showToastNotification(
context,
message: LocaleKeys.settings_sites_error_updatePathNameFailed.tr(),
type: ToastificationType.error,
description: error.code.publishErrorMessage,
);
},
);
}
}
}
class _PublishedWidget extends StatefulWidget {
const _PublishedWidget({
required this.url,
2024-10-31 14:38:32 +08:00
required this.pathName,
required this.namespace,
required this.onVisitSite,
required this.onUnPublish,
});
final String url;
2024-10-31 14:38:32 +08:00
final String pathName;
final String namespace;
final void Function(String url) onVisitSite;
final VoidCallback onUnPublish;
@override
State<_PublishedWidget> createState() => _PublishedWidgetState();
}
class _PublishedWidgetState extends State<_PublishedWidget> {
final controller = TextEditingController();
@override
void initState() {
super.initState();
2024-10-31 14:38:32 +08:00
controller.text = widget.pathName;
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const VSpace(16),
const _PublishTabHeader(),
const VSpace(16),
_PublishUrl(
2024-10-31 14:38:32 +08:00
namespace: widget.namespace,
controller: controller,
2024-10-31 14:38:32 +08:00
onCopy: (_) {
final url = context.read<ShareBloc>().state.url;
getIt<ClipboardService>().setData(
ClipboardServiceData(plainText: url),
);
showToastNotification(
context,
message: LocaleKeys.grid_url_copy.tr(),
);
},
2024-10-31 14:38:32 +08:00
onSubmitted: (pathName) {
context.read<ShareBloc>().add(ShareEvent.updatePathName(pathName));
},
),
const VSpace(16),
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Spacer(),
2024-10-31 14:38:32 +08:00
UnPublishButton(
onUnPublish: widget.onUnPublish,
),
const HSpace(6),
_buildVisitSiteButton(),
],
),
],
);
}
Widget _buildVisitSiteButton() {
return RoundedTextButton(
2024-10-31 14:38:32 +08:00
width: 108,
height: 36,
2024-10-31 14:38:32 +08:00
onPressed: () {
final url = context.read<ShareBloc>().state.url;
widget.onVisitSite(url);
},
title: LocaleKeys.shareAction_visitSite.tr(),
borderRadius: const BorderRadius.all(Radius.circular(10)),
fillColor: Theme.of(context).colorScheme.primary,
hoverColor: Theme.of(context).colorScheme.primary.withOpacity(0.9),
textColor: Theme.of(context).colorScheme.onPrimary,
);
}
}
2024-10-31 14:38:32 +08:00
class UnPublishButton extends StatelessWidget {
const UnPublishButton({
super.key,
required this.onUnPublish,
});
final VoidCallback onUnPublish;
@override
Widget build(BuildContext context) {
return SizedBox(
width: 108,
height: 36,
child: FlowyButton(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(color: ShareMenuColors.borderColor(context)),
),
radius: BorderRadius.circular(10),
text: FlowyText.regular(
lineHeight: 1.0,
LocaleKeys.shareAction_unPublish.tr(),
textAlign: TextAlign.center,
),
onTap: onUnPublish,
),
);
}
}
class _PublishWidget extends StatefulWidget {
const _PublishWidget({
required this.onPublish,
});
final void Function(List<ViewPB> selectedViews) onPublish;
@override
State<_PublishWidget> createState() => _PublishWidgetState();
}
class _PublishWidgetState extends State<_PublishWidget> {
List<ViewPB> _selectedViews = [];
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const VSpace(16),
const _PublishTabHeader(),
const VSpace(16),
// if current view is a database, show the database selector
if (context.read<ShareBloc>().view.layout.isDatabaseView) ...[
_PublishDatabaseSelector(
view: context.read<ShareBloc>().view,
onSelected: (selectedDatabases) {
_selectedViews = selectedDatabases;
},
),
const VSpace(16),
],
2024-10-31 14:38:32 +08:00
PublishButton(
onPublish: () {
if (context.read<ShareBloc>().view.layout.isDatabaseView) {
// check if any database is selected
if (_selectedViews.isEmpty) {
showToastNotification(
context,
message: LocaleKeys.publish_noDatabaseSelected.tr(),
);
return;
}
}
widget.onPublish(_selectedViews);
},
),
],
);
}
}
2024-10-31 14:38:32 +08:00
class PublishButton extends StatelessWidget {
const PublishButton({
super.key,
required this.onPublish,
});
final VoidCallback onPublish;
@override
Widget build(BuildContext context) {
return PrimaryRoundedButton(
text: LocaleKeys.shareAction_publish.tr(),
useIntrinsicWidth: false,
margin: const EdgeInsets.symmetric(vertical: 9.0),
fontSize: 14.0,
figmaLineHeight: 18.0,
onTap: onPublish,
);
}
}
class _PublishTabHeader extends StatelessWidget {
const _PublishTabHeader();
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const FlowySvg(FlowySvgs.share_publish_s),
const HSpace(6),
FlowyText(LocaleKeys.shareAction_publishToTheWeb.tr()),
],
),
const VSpace(4),
FlowyText.regular(
LocaleKeys.shareAction_publishToTheWebHint.tr(),
fontSize: 12,
maxLines: 3,
color: Theme.of(context).hintColor,
),
],
);
}
}
2024-10-31 14:38:32 +08:00
class _PublishUrl extends StatefulWidget {
const _PublishUrl({
2024-10-31 14:38:32 +08:00
required this.namespace,
required this.controller,
required this.onCopy,
required this.onSubmitted,
});
2024-10-31 14:38:32 +08:00
final String namespace;
final TextEditingController controller;
final void Function(String url) onCopy;
final void Function(String url) onSubmitted;
2024-10-31 14:38:32 +08:00
@override
State<_PublishUrl> createState() => _PublishUrlState();
}
class _PublishUrlState extends State<_PublishUrl> {
final focusNode = FocusNode();
bool showSaveButton = false;
@override
void initState() {
super.initState();
2024-11-04 03:26:48 +01:00
focusNode.addListener(_onFocusChanged);
2024-10-31 14:38:32 +08:00
}
2024-11-04 03:26:48 +01:00
void _onFocusChanged() => setState(() => showSaveButton = focusNode.hasFocus);
2024-10-31 14:38:32 +08:00
@override
void dispose() {
2024-11-04 03:26:48 +01:00
focusNode.removeListener(_onFocusChanged);
2024-10-31 14:38:32 +08:00
focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: 36,
child: FlowyTextField(
autoFocus: false,
2024-10-31 14:38:32 +08:00
controller: widget.controller,
focusNode: focusNode,
enableBorderColor: ShareMenuColors.borderColor(context),
2024-10-31 14:38:32 +08:00
prefixIcon: _buildPrefixIcon(context),
suffixIcon: _buildSuffixIcon(context),
textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: 14,
height: 18.0 / 14.0,
),
),
);
}
Widget _buildPrefixIcon(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 230),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const HSpace(8.0),
Flexible(
child: FlowyText.regular(
ShareConstants.buildNamespaceUrl(
nameSpace: '${widget.namespace}/',
),
fontSize: 14,
figmaLineHeight: 18.0,
overflow: TextOverflow.ellipsis,
),
),
const HSpace(6.0),
const Padding(
padding: EdgeInsets.symmetric(vertical: 2.0),
child: VerticalDivider(
thickness: 1.0,
width: 1.0,
),
),
const HSpace(6.0),
],
),
);
}
Widget _buildSuffixIcon(BuildContext context) {
return showSaveButton
? _buildSaveButton(context)
: _buildCopyLinkIcon(context);
}
Widget _buildSaveButton(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
child: FlowyButton(
useIntrinsicWidth: true,
text: FlowyText.regular(
LocaleKeys.button_save.tr(),
figmaLineHeight: 18.0,
),
onTap: () {
widget.onSubmitted(widget.controller.text);
focusNode.unfocus();
},
),
);
}
Widget _buildCopyLinkIcon(BuildContext context) {
return FlowyHover(
2024-10-31 14:38:32 +08:00
style: const HoverStyle(
contentMargin: EdgeInsets.all(4),
),
child: GestureDetector(
2024-10-31 14:38:32 +08:00
behavior: HitTestBehavior.opaque,
onTap: () => widget.onCopy(widget.controller.text),
child: Container(
2024-10-31 14:38:32 +08:00
width: 32,
height: 32,
alignment: Alignment.center,
2024-10-31 14:38:32 +08:00
padding: const EdgeInsets.all(6),
decoration: const BoxDecoration(
border: Border(left: BorderSide(color: Color(0x141F2329))),
),
child: const FlowySvg(
FlowySvgs.m_toolbar_link_m,
),
),
),
);
}
}
// used to select which database view should be published
class _PublishDatabaseSelector extends StatefulWidget {
const _PublishDatabaseSelector({
required this.view,
required this.onSelected,
});
final ViewPB view;
final void Function(List<ViewPB> selectedDatabases) onSelected;
@override
State<_PublishDatabaseSelector> createState() =>
_PublishDatabaseSelectorState();
}
class _PublishDatabaseSelectorState extends State<_PublishDatabaseSelector> {
final PropertyValueNotifier<List<(ViewPB, bool)>> _databaseStatus =
PropertyValueNotifier<List<(ViewPB, bool)>>([]);
late final _borderColor = Theme.of(context).hintColor.withOpacity(0.3);
@override
void initState() {
super.initState();
2024-11-04 03:26:48 +01:00
_databaseStatus.addListener(_onDatabaseStatusChanged);
_databaseStatus.value = context
.read<DatabaseTabBarBloc>()
.state
.tabBars
.map((e) => (e.view, true))
.toList();
}
2024-11-04 03:26:48 +01:00
void _onDatabaseStatusChanged() {
final selectedDatabases =
_databaseStatus.value.where((e) => e.$2).map((e) => e.$1).toList();
widget.onSelected(selectedDatabases);
}
@override
void dispose() {
2024-11-04 03:26:48 +01:00
_databaseStatus.removeListener(_onDatabaseStatusChanged);
_databaseStatus.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>(
builder: (context, state) {
return Container(
clipBehavior: Clip.antiAlias,
decoration: ShapeDecoration(
shape: RoundedRectangleBorder(
side: BorderSide(color: _borderColor),
borderRadius: BorderRadius.circular(8),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const VSpace(10),
_buildSelectedDatabaseCount(context),
const VSpace(10),
_buildDivider(context),
const VSpace(10),
...state.tabBars.map(
(e) => _buildDatabaseSelector(context, e),
),
],
),
);
},
);
}
Widget _buildDivider(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Divider(
color: _borderColor,
thickness: 1,
height: 1,
),
);
}
Widget _buildSelectedDatabaseCount(BuildContext context) {
return ValueListenableBuilder(
valueListenable: _databaseStatus,
builder: (context, selectedDatabases, child) {
final count = selectedDatabases.where((e) => e.$2).length;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: FlowyText(
LocaleKeys.publish_database.plural(count).tr(),
color: Theme.of(context).hintColor,
fontSize: 13,
),
);
},
);
}
Widget _buildDatabaseSelector(BuildContext context, DatabaseTabBar tabBar) {
final isPrimaryDatabase = tabBar.view.id == widget.view.id;
return ValueListenableBuilder(
valueListenable: _databaseStatus,
builder: (context, selectedDatabases, child) {
final isSelected = selectedDatabases.any(
(e) => e.$1.id == tabBar.view.id && e.$2,
);
return _DatabaseSelectorItem(
tabBar: tabBar,
isSelected: isSelected,
isPrimaryDatabase: isPrimaryDatabase,
onTap: () {
// unable to deselect the primary database
if (isPrimaryDatabase) {
showToastNotification(
context,
message:
LocaleKeys.publish_unableToDeselectPrimaryDatabase.tr(),
);
return;
}
// toggle the selection status
_databaseStatus.value = _databaseStatus.value
.map(
(e) =>
e.$1.id == tabBar.view.id ? (e.$1, !e.$2) : (e.$1, e.$2),
)
.toList();
},
);
},
);
}
}
class _DatabaseSelectorItem extends StatelessWidget {
const _DatabaseSelectorItem({
required this.tabBar,
required this.isSelected,
required this.onTap,
required this.isPrimaryDatabase,
});
final DatabaseTabBar tabBar;
final bool isSelected;
final VoidCallback onTap;
final bool isPrimaryDatabase;
@override
Widget build(BuildContext context) {
Widget child = _buildItem(context);
if (!isPrimaryDatabase) {
child = FlowyHover(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: child,
),
);
} else {
child = FlowyTooltip(
message: LocaleKeys.publish_mustSelectPrimaryDatabase.tr(),
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: child,
),
);
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
child: child,
);
}
Widget _buildItem(BuildContext context) {
final svg = isPrimaryDatabase
? FlowySvgs.unable_select_s
: isSelected
? FlowySvgs.check_filled_s
: FlowySvgs.uncheck_s;
final blendMode = isPrimaryDatabase ? BlendMode.srcIn : null;
return Container(
height: 30,
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
children: [
FlowySvg(
svg,
blendMode: blendMode,
size: const Size.square(18),
),
const HSpace(9.0),
FlowySvg(
tabBar.view.layout.icon,
size: const Size.square(16),
),
const HSpace(6.0),
FlowyText.regular(
tabBar.view.name,
fontSize: 14,
overflow: TextOverflow.ellipsis,
),
],
),
);
}
}