feat: adjust cover plugin and support recent section on mobile platform (#3921)

This commit is contained in:
Lucas.Xu 2023-11-13 10:07:46 +08:00 committed by GitHub
parent 765103dd22
commit 7cee8e392f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 734 additions and 224 deletions

View File

@ -48,6 +48,9 @@ PODS:
- fluttertoast (0.0.2):
- Flutter
- Toast
- FMDB (2.7.5):
- FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5)
- image_gallery_saver (2.0.2):
- Flutter
- image_picker_ios (0.0.1):
@ -72,6 +75,9 @@ PODS:
- FlutterMacOS
- sign_in_with_apple (0.0.1):
- Flutter
- sqflite (0.0.3):
- Flutter
- FMDB (>= 2.7.5)
- super_native_extensions (0.0.1):
- Flutter
- SwiftyGif (5.4.3)
@ -99,6 +105,7 @@ DEPENDENCIES:
- rich_clipboard_ios (from `.symlinks/plugins/rich_clipboard_ios/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`)
- super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
@ -107,6 +114,7 @@ SPEC REPOS:
trunk:
- DKImagePickerController
- DKPhotoGallery
- FMDB
- ReachabilitySwift
- SDWebImage
- SwiftyGif
@ -147,6 +155,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sign_in_with_apple:
:path: ".symlinks/plugins/sign_in_with_apple/ios"
sqflite:
:path: ".symlinks/plugins/sqflite/ios"
super_native_extensions:
:path: ".symlinks/plugins/super_native_extensions/ios"
url_launcher_ios:
@ -165,6 +175,7 @@ SPEC CHECKSUMS:
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5
integration_test: 13825b8a9334a850581300559b8839134b124670
@ -176,6 +187,7 @@ SPEC CHECKSUMS:
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
shared_preferences_foundation: e2dae3258e06f44cc55f49d42024fd8dd03c590c
sign_in_with_apple: f3bf75217ea4c2c8b91823f225d70230119b8440
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196

View File

@ -1,68 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSCameraUsageDescription</key>
<string>AppFlowy requires access to the camera.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>AppFlowy requires access to the photo library.</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
</array>
<key>CFBundleName</key>
<string>AppFlowy</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string></string>
<key>CFBundleURLSchemes</key>
<array>
<string>appflowy-flutter</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>
<dict>
<key>NSCameraUsageDescription</key>
<string>AppFlowy requires access to the camera.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>AppFlowy requires access to the photo library.</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true />
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
</array>
<key>FLTEnableImpeller</key>
<false />
<key>CFBundleName</key>
<string>AppFlowy</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string></string>
<key>CFBundleURLSchemes</key>
<array>
<string>appflowy-flutter</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true />
<key>UIApplicationSupportsIndirectInputEvents</key>
<true />
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false />
</dict>
</plist>

View File

@ -2,12 +2,22 @@ import 'package:appflowy/mobile/presentation/database/mobile_board_screen.dart';
import 'package:appflowy/mobile/presentation/database/mobile_calendar_screen.dart';
import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class MobileRouterRecord {
PropertyValueNotifier<String> lastPushedRouter =
PropertyValueNotifier<String>('');
}
extension MobileRouter on BuildContext {
Future<void> pushView(ViewPB view) async {
await FolderEventSetLatestView(ViewIdPB(value: view.id)).send();
getIt<MobileRouterRecord>().lastPushedRouter.value = view.routeName;
push(
Uri(
path: view.routeName,

View File

@ -117,15 +117,19 @@ class _MobileViewPageState extends State<MobileViewPage> {
appBar: AppBar(
titleSpacing: 0,
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null)
FlowyText(
'$icon ',
fontSize: 22.0,
),
FlowyText.regular(
view?.name ?? widget.title ?? '',
fontSize: 14.0,
Expanded(
child: FlowyText.regular(
view?.name ?? widget.title ?? '',
fontSize: 14.0,
overflow: TextOverflow.ellipsis,
),
),
],
),

View File

@ -2,7 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/home/mobile_folders.dart';
import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart';
import 'package:appflowy/mobile/presentation/home/mobile_home_page_recent_files.dart';
import 'package:appflowy/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart';
@ -97,8 +97,7 @@ class MobileHomePage extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
// Recent files
const MobileHomePageRecentFilesWidget(),
const Divider(),
const MobileRecentFolder(),
// Folders
Padding(

View File

@ -1,122 +0,0 @@
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
// TODO(yijing): replace by real data later
class MockRecentFile {
MockRecentFile({
required this.title,
});
final String title;
final String icon = '🐼';
final image = Image.asset(
'assets/images/app_flowy_abstract_cover_1.jpg',
fit: BoxFit.cover,
);
}
final recentFilesList = <MockRecentFile>[
MockRecentFile(title: 'Work out plan'),
MockRecentFile(title: 'Travel plan'),
MockRecentFile(title: 'Meeting notes'),
MockRecentFile(title: 'Recipes'),
MockRecentFile(title: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'),
];
class MobileHomePageRecentFilesWidget extends StatelessWidget {
const MobileHomePageRecentFilesWidget({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
// TODO: implement the details later.
return SizedBox(
height: 168,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: FlowyText.semibold(
'Recent',
fontSize: 20.0,
),
),
Expanded(
child: ListView.separated(
separatorBuilder: (context, index) => const HSpace(8),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
scrollDirection: Axis.horizontal,
itemCount: recentFilesList.length,
itemBuilder: (context, index) {
return Container(
width: 120,
decoration: BoxDecoration(
color: theme.colorScheme.background,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: theme.colorScheme.outline.withOpacity(0.5),
),
),
child: Stack(
children: [
Align(
alignment: Alignment.topCenter,
child: SizedBox(
height: 60,
width: double.infinity,
child: ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
child: recentFilesList[index].image,
),
),
),
Align(
alignment: Alignment.centerLeft,
child: Container(
height: 32,
width: 32,
margin: const EdgeInsets.only(left: 8),
child: Text(
recentFilesList[index].icon,
style: const TextStyle(fontSize: 32),
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
height: 32,
width: double.infinity,
margin: const EdgeInsets.only(
left: 8,
right: 8,
bottom: 8,
),
child: Text(
recentFilesList[index].title,
softWrap: true,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onBackground,
),
maxLines: 2,
),
),
),
],
),
);
},
),
),
],
),
);
}
}

View File

@ -0,0 +1,104 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/home/recent_folder/mobile_recent_view.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:dartz/dartz.dart' hide State;
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
class MobileRecentFolder extends StatefulWidget {
const MobileRecentFolder({super.key});
@override
State<MobileRecentFolder> createState() => _MobileRecentFolderState();
}
class _MobileRecentFolderState extends State<MobileRecentFolder> {
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: getIt<MobileRouterRecord>().lastPushedRouter,
builder: (context, value, child) {
return FutureBuilder<Either<RepeatedViewPB, FlowyError>>(
future: FolderEventReadRecentViews().send(),
builder: (context, snapshot) {
final recentViews = snapshot.data
?.fold<List<ViewPB>>(
(l) => l.items,
(r) => [],
)
// only keep the first 10 items.
.reversed
.take(10)
.toList();
if (recentViews == null || recentViews.isEmpty) {
return const SizedBox.shrink();
}
return Column(
children: [
_RecentViews(
key: ValueKey(recentViews),
// the recent views are in reverse order
recentViews: recentViews,
),
const VSpace(12.0)
],
);
},
);
},
);
}
}
class _RecentViews extends StatelessWidget {
const _RecentViews({
super.key,
required this.recentViews,
});
final List<ViewPB> recentViews;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 168,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: FlowyText.semibold(
LocaleKeys.sideBar_recent.tr(),
fontSize: 20.0,
),
),
Expanded(
child: ListView.separated(
separatorBuilder: (context, index) => const HSpace(8),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
scrollDirection: Axis.horizontal,
itemCount: recentViews.length,
itemBuilder: (context, index) {
return MobileRecentView(
view: recentViews[index],
height: 120,
);
},
),
),
],
),
);
}
}

View File

@ -0,0 +1,208 @@
import 'dart:io';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/workspace/application/doc/doc_listener.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:string_validator/string_validator.dart';
class MobileRecentView extends StatefulWidget {
const MobileRecentView({
super.key,
required this.view,
required this.height,
});
final ViewPB view;
final double height;
@override
State<MobileRecentView> createState() => _MobileRecentViewState();
}
class _MobileRecentViewState extends State<MobileRecentView> {
late final ViewListener viewListener;
late ViewPB view;
late final DocumentListener documentListener;
@override
void initState() {
super.initState();
view = widget.view;
viewListener = ViewListener(
viewId: view.id,
)..start(
onViewUpdated: (view) {
setState(() {
this.view = view;
});
},
);
documentListener = DocumentListener(id: view.id)
..start(
didReceiveUpdate: (document) {
setState(() {
view = view;
});
},
);
}
@override
void dispose() {
viewListener.stop();
documentListener.stop();
super.dispose();
}
@override
Widget build(BuildContext context) {
final icon = view.icon.value;
final theme = Theme.of(context);
return GestureDetector(
onTap: () => context.pushView(view),
child: Container(
height: widget.height,
width: widget.height,
decoration: BoxDecoration(
color: theme.colorScheme.background,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: theme.colorScheme.outline.withOpacity(0.5),
),
),
child: Stack(
children: [
Positioned(
top: 0,
left: 0,
right: 0,
child: SizedBox(
height: widget.height / 2.0,
width: double.infinity,
child: ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
child: _buildCoverWidget(),
),
),
),
Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.only(left: 4),
child: icon.isNotEmpty
? FlowyText(
icon,
fontSize: 30.0,
)
: SizedBox.square(
dimension: 32.0,
child: view.defaultIcon(),
),
),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
height: widget.height / 2.0,
width: double.infinity,
padding: const EdgeInsets.only(
left: 8.0,
top: 14.0,
right: 8.0,
),
child: FlowyText(
view.name,
maxLines: 2,
fontSize: 16.0,
overflow: TextOverflow.ellipsis,
),
),
),
],
),
),
);
}
Widget _buildCoverWidget() {
return FutureBuilder<Node?>(
future: _getPageNode(),
builder: ((context, snapshot) {
final node = snapshot.data;
final placeholder = Container(
color: Theme.of(context).colorScheme.onSecondaryContainer,
);
if (node == null) {
return placeholder;
}
final type = CoverType.fromString(
node.attributes[DocumentHeaderBlockKeys.coverType],
);
final cover =
node.attributes[DocumentHeaderBlockKeys.coverDetails] as String?;
if (cover == null) {
return placeholder;
}
switch (type) {
case CoverType.file:
if (isURL(cover)) {
return CachedNetworkImage(
imageUrl: cover,
fit: BoxFit.cover,
);
}
final imageFile = File(cover);
if (!imageFile.existsSync()) {
return placeholder;
}
return Image.file(
imageFile,
);
case CoverType.asset:
return Image.asset(
cover,
fit: BoxFit.cover,
);
case CoverType.color:
final color = cover.tryToColor() ?? Colors.white;
return Container(
color: color,
);
case CoverType.none:
return placeholder;
}
}),
);
}
Future<Node?> _getPageNode() async {
final data = await DocumentEventGetDocumentData(
OpenDocumentPayloadPB(documentId: view.id),
).send();
final document = data.fold((l) => l.toDocument(), (r) => null);
if (document != null) {
return document.root;
}
return null;
}
}

View File

@ -8,6 +8,7 @@ Future<T?> showFlowyMobileBottomSheet<T>(
}) async {
return showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
child: Column(

View File

@ -529,14 +529,12 @@ class ColorItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
return InkWell(
customBorder: const RoundedRectangleBorder(
borderRadius: Corners.s6Border,
),
hoverColor: hoverColor,
onTap: () => onTap(option.colorHex),
child: Padding(
padding: const EdgeInsets.only(right: 10.0),
return Padding(
padding: const EdgeInsets.only(right: 10.0),
child: InkWell(
customBorder: const CircleBorder(),
hoverColor: hoverColor,
onTap: () => onTap(option.colorHex),
child: SizedBox.square(
dimension: 25,
child: DecoratedBox(

View File

@ -2,19 +2,23 @@ import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy/workspace/application/view/view_listener.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide UploadImageMenu;
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/rounded_button.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:string_validator/string_validator.dart';
import 'cover_editor.dart';
@ -262,7 +266,9 @@ class _DocumentHeaderToolbarState extends State<DocumentHeaderToolbar> {
FlowyButton(
leftIconSize: const Size.square(18),
onTap: () => widget.onCoverChanged(
cover: (CoverType.asset, builtInAssetImages.first),
cover: PlatformExtension.isDesktopOrWeb
? (CoverType.asset, builtInAssetImages.first)
: (CoverType.color, '0xffe8e0ff'),
),
useIntrinsicWidth: true,
leftIcon: const FlowySvg(FlowySvgs.image_s),
@ -373,6 +379,12 @@ class DocumentCoverState extends State<DocumentCover> {
@override
Widget build(BuildContext context) {
return PlatformExtension.isDesktopOrWeb
? _buildDesktopCover()
: _buildMobileCover();
}
Widget _buildDesktopCover() {
return SizedBox(
height: kCoverHeight,
child: MouseRegion(
@ -393,10 +405,82 @@ class DocumentCoverState extends State<DocumentCover> {
);
}
Widget _buildMobileCover() {
return SizedBox(
height: kCoverHeight,
child: Stack(
children: [
SizedBox(
height: double.infinity,
width: double.infinity,
child: _buildCoverImage(),
),
Positioned(
bottom: 8,
right: 12,
child: RoundedTextButton(
onPressed: () {
showFlowyMobileBottomSheet(
context,
title: LocaleKeys.document_plugins_cover_changeCover.tr(),
builder: (context) {
return ConstrainedBox(
constraints: const BoxConstraints(
maxHeight: 340,
minHeight: 80,
),
child: UploadImageMenu(
supportTypes: const [
UploadImageType.color,
UploadImageType.local,
UploadImageType.url,
UploadImageType.unsplash,
],
onSelectedLocalImage: (path) async {
context.pop();
widget.onCoverChanged(CoverType.file, path);
},
onSelectedAIImage: (_) {
throw UnimplementedError();
},
onSelectedNetworkImage: (url) async {
context.pop();
widget.onCoverChanged(CoverType.file, url);
},
onSelectedColor: (color) {
context.pop();
widget.onCoverChanged(CoverType.color, color);
},
),
);
},
);
},
fillColor: Theme.of(context).colorScheme.onSurfaceVariant,
width: 120,
height: 32,
title: LocaleKeys.document_plugins_cover_changeCover.tr(),
),
),
],
),
);
}
Widget _buildCoverImage() {
final detail = widget.coverDetails;
if (detail == null) {
return const SizedBox.shrink();
}
switch (widget.coverType) {
case CoverType.file:
final imageFile = File(widget.coverDetails ?? "");
if (isURL(detail)) {
return CachedNetworkImage(
imageUrl: detail,
fit: BoxFit.cover,
);
}
final imageFile = File(detail);
if (!imageFile.existsSync()) {
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onCoverChanged(CoverType.none, null);

View File

@ -0,0 +1,41 @@
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class ImagePickerPage extends StatefulWidget {
const ImagePickerPage({
super.key,
// required this.onSelected,
});
// final void Function(EmojiPickerResult) onSelected;
@override
State<ImagePickerPage> createState() => _ImagePickerPageState();
}
class _ImagePickerPageState extends State<ImagePickerPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
titleSpacing: 0,
title: const FlowyText.semibold(
'Page icon',
fontSize: 14.0,
),
leading: AppBarBackButton(
onTap: () => context.pop(),
),
),
body: SafeArea(
child: UploadImageMenu(
onSubmitted: (_) {},
onUpload: (_) {},
),
),
);
}
}

View File

@ -0,0 +1,15 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/flowy_image_picker.dart';
import 'package:flutter/material.dart';
class MobileImagePickerScreen extends StatelessWidget {
static const routeName = '/image_picker';
const MobileImagePickerScreen({
super.key,
});
@override
Widget build(BuildContext context) {
return const ImagePickerPage();
}
}

View File

@ -1,12 +1,15 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/embed_image_url_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/open_ai_image_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/stability_ai_image_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_file_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/util/platform_extension.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide ColorOption;
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
@ -16,7 +19,8 @@ enum UploadImageType {
url,
unsplash,
stabilityAI,
openAI;
openAI,
color;
String get description {
switch (this) {
@ -30,6 +34,8 @@ enum UploadImageType {
return LocaleKeys.document_imageBlock_ai_label.tr();
case UploadImageType.stabilityAI:
return LocaleKeys.document_imageBlock_stability_ai_label.tr();
case UploadImageType.color:
return LocaleKeys.document_plugins_cover_colors.tr();
}
}
}
@ -40,12 +46,14 @@ class UploadImageMenu extends StatefulWidget {
required this.onSelectedLocalImage,
required this.onSelectedAIImage,
required this.onSelectedNetworkImage,
this.onSelectedColor,
this.supportTypes = UploadImageType.values,
});
final void Function(String? path) onSelectedLocalImage;
final void Function(String url) onSelectedAIImage;
final void Function(String url) onSelectedNetworkImage;
final void Function(String color)? onSelectedColor;
final List<UploadImageType> supportTypes;
@override
@ -128,18 +136,23 @@ class _UploadImageMenuState extends State<UploadImageMenu> {
}
Widget _buildTab() {
final type = UploadImageType.values[currentTabIndex];
final constraints =
PlatformExtension.isMobile ? const BoxConstraints(minHeight: 92) : null;
final type = values[currentTabIndex];
switch (type) {
case UploadImageType.local:
return Padding(
return Container(
padding: const EdgeInsets.all(8.0),
alignment: Alignment.center,
constraints: constraints,
child: UploadImageFileWidget(
onPickFile: widget.onSelectedLocalImage,
),
);
case UploadImageType.url:
return Padding(
return Container(
padding: const EdgeInsets.all(8.0),
constraints: constraints,
child: EmbedImageUrlWidget(
onSubmit: widget.onSelectedNetworkImage,
),
@ -156,8 +169,9 @@ class _UploadImageMenuState extends State<UploadImageMenu> {
case UploadImageType.openAI:
return supportOpenAI
? Expanded(
child: Padding(
child: Container(
padding: const EdgeInsets.all(8.0),
constraints: constraints,
child: OpenAIImageWidget(
onSelectNetworkImage: widget.onSelectedAIImage,
),
@ -172,7 +186,7 @@ class _UploadImageMenuState extends State<UploadImageMenu> {
case UploadImageType.stabilityAI:
return supportStabilityAI
? Expanded(
child: Padding(
child: Container(
padding: const EdgeInsets.all(8.0),
child: StabilityAIImageWidget(
onSelectImage: widget.onSelectedLocalImage,
@ -186,6 +200,28 @@ class _UploadImageMenuState extends State<UploadImageMenu> {
.tr(),
),
);
case UploadImageType.color:
final theme = Theme.of(context);
return Container(
constraints: constraints,
padding: const EdgeInsets.all(8.0),
alignment: Alignment.center,
child: CoverColorPicker(
pickerBackgroundColor: theme.cardColor,
pickerItemHoverColor: theme.hoverColor,
backgroundColorOptions: FlowyTint.values
.map<ColorOption>(
(t) => ColorOption(
colorHex: t.color(context).toHex(),
name: t.tintName(AppFlowyEditorL10n.current),
),
)
.toList(),
onSubmittedBackgroundColorHex: (color) {
widget.onSelectedColor?.call(color);
},
),
);
}
}
}

View File

@ -1,6 +1,7 @@
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/network_monitor.dart';
import 'package:appflowy/env/env.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/plugins/document/application/prelude.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
@ -143,6 +144,7 @@ void _resolveHomeDeps(GetIt getIt) {
getIt.registerSingleton(FToast());
getIt.registerSingleton(MenuSharedState());
getIt.registerSingleton(MobileRouterRecord());
getIt.registerFactoryParam<UserListener, UserProfilePB, void>(
(user, _) => UserListener(userProfile: user),

View File

@ -4,6 +4,7 @@ import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart';
import 'package:appflowy/mobile/presentation/favorite/mobile_favorite_page.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_picker_screen.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/startup/tasks/app_widget.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
@ -51,6 +52,7 @@ GoRouter generateRouter(Widget child) {
// emoji picker
_mobileEmojiPickerPageRoute(),
_mobileImagePickerPageRoute(),
],
// Desktop and Mobile
@ -216,6 +218,18 @@ GoRoute _mobileEmojiPickerPageRoute() {
);
}
GoRoute _mobileImagePickerPageRoute() {
return GoRoute(
parentNavigatorKey: AppGlobals.rootNavKey,
path: MobileImagePickerScreen.routeName,
pageBuilder: (context, state) {
return const MaterialPage(
child: MobileImagePickerScreen(),
);
},
);
}
GoRoute _desktopHomeScreenRoute() {
return GoRoute(
path: DesktopHomeScreen.routeName,

View File

@ -178,6 +178,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "8.6.0"
cached_network_image:
dependency: "direct main"
description:
name: cached_network_image
sha256: f98972704692ba679db144261172a8e20feb145636c617af0eb4022132a6797f
url: "https://pub.dev"
source: hosted
version: "3.3.0"
cached_network_image_platform_interface:
dependency: transitive
description:
name: cached_network_image_platform_interface
sha256: "56aa42a7a01e3c9db8456d9f3f999931f1e05535b5a424271e9a38cabf066613"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
cached_network_image_web:
dependency: transitive
description:
name: cached_network_image_web
sha256: "759b9a9f8f6ccbb66c185df805fac107f05730b1dab9c64626d1008cca532257"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
calendar_view:
dependency: "direct main"
description:
@ -539,6 +563,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "8.1.3"
flutter_cache_manager:
dependency: transitive
description:
name: flutter_cache_manager
sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba"
url: "https://pub.dev"
source: hosted
version: "3.3.1"
flutter_colorpicker:
dependency: "direct main"
description:
@ -1082,6 +1114,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.2"
octo_image:
dependency: transitive
description:
name: octo_image
sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
package_config:
dependency: transitive
description:
@ -1575,6 +1615,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.1"
sqflite:
dependency: transitive
description:
name: sqflite
sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
sha256: "8ed044102f3135add97be8653662052838859f5400075ef227f8ad72ae320803"
url: "https://pub.dev"
source: hosted
version: "2.5.0+1"
stack_trace:
dependency: transitive
description:
@ -1672,6 +1728,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.3.1"
synchronized:
dependency: transitive
description:
name: synchronized
sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
table_calendar:
dependency: "direct main"
description:

View File

@ -124,6 +124,7 @@ dependencies:
flutter_slidable: ^3.0.0
image_picker: ^1.0.4
image_gallery_saver: ^2.0.3
cached_network_image: ^3.3.0
dev_dependencies:
flutter_lints: ^2.0.1

View File

@ -186,7 +186,8 @@
"favorites": "Favorites",
"clickToHidePersonal": "Click to hide personal section",
"clickToHideFavorites": "Click to hide favorite section",
"addAPage": "Add a page"
"addAPage": "Add a page",
"recent": "Recent"
},
"notifications": {
"export": {

View File

@ -228,6 +228,22 @@ pub(crate) async fn read_favorites_handler(
}
data_result_ok(RepeatedViewPB { items: views })
}
#[tracing::instrument(level = "debug", skip(folder), err)]
pub(crate) async fn read_recent_views_handler(
folder: AFPluginState<Weak<FolderManager>>,
) -> DataResult<RepeatedViewPB, FlowyError> {
let folder = upgrade_folder(folder)?;
let recent_items = folder.get_all_recent_sections().await;
let mut views = vec![];
for item in recent_items {
if let Ok(view) = folder.get_view_pb(&item.id).await {
views.push(view);
}
}
data_result_ok(RepeatedViewPB { items: views })
}
#[tracing::instrument(level = "debug", skip(folder), err)]
pub(crate) async fn read_trash_handler(
folder: AFPluginState<Weak<FolderManager>>,

View File

@ -36,6 +36,7 @@ pub fn init(folder: Weak<FolderManager>) -> AFPlugin {
.event(FolderEvent::GetFolderSnapshots, get_folder_snapshots_handler)
.event(FolderEvent::UpdateViewIcon, update_view_icon_handler)
.event(FolderEvent::ReadFavorites, read_favorites_handler)
.event(FolderEvent::ReadRecentViews, read_recent_views_handler)
.event(FolderEvent::ToggleFavorite, toggle_favorites_handler)
}
@ -145,4 +146,7 @@ pub enum FolderEvent {
#[event(input = "UpdateViewIconPayloadPB")]
UpdateViewIcon = 35,
#[event(output = "RepeatedViewPB")]
ReadRecentViews = 36,
}

View File

@ -7,8 +7,8 @@ use collab::core::collab::{CollabRawData, MutexCollab};
use collab::core::collab_state::SyncState;
use collab_entity::CollabType;
use collab_folder::{
Folder, FolderData, FolderNotify, SectionItem, TrashChange, TrashChangeReceiver, TrashInfo,
UserId, View, ViewChange, ViewChangeReceiver, ViewLayout, ViewUpdate, Workspace,
Folder, FolderData, FolderNotify, Section, SectionItem, TrashChange, TrashChangeReceiver,
TrashInfo, UserId, View, ViewChange, ViewChangeReceiver, ViewLayout, ViewUpdate, Workspace,
};
use parking_lot::{Mutex, RwLock};
use tokio_stream::wrappers::WatchStream;
@ -745,6 +745,7 @@ impl FolderManager {
|| Err(FlowyError::record_not_found()),
|folder| {
folder.set_current_view(view_id);
folder.add_recent_view_ids(vec![view_id.to_string()]);
Ok(folder.get_workspace_id())
},
)?;
@ -800,17 +801,12 @@ impl FolderManager {
#[tracing::instrument(level = "trace", skip(self))]
pub(crate) async fn get_all_favorites(&self) -> Vec<SectionItem> {
self.with_folder(Vec::new, |folder| {
let trash_ids = folder
.get_all_trash()
.into_iter()
.map(|trash| trash.id)
.collect::<Vec<String>>();
self.get_sections(Section::Favorite)
}
let mut views = folder.get_all_favorites();
views.retain(|view| !trash_ids.contains(&view.id));
views
})
#[tracing::instrument(level = "trace", skip(self))]
pub(crate) async fn get_all_recent_sections(&self) -> Vec<SectionItem> {
self.get_sections(Section::Recent)
}
#[tracing::instrument(level = "trace", skip(self))]
@ -1039,6 +1035,26 @@ impl FolderManager {
pub fn get_cloud_service(&self) -> &Arc<dyn FolderCloudService> {
&self.cloud_service
}
fn get_sections(&self, section_type: Section) -> Vec<SectionItem> {
self.with_folder(Vec::new, |folder| {
let trash_ids = folder
.get_all_trash()
.into_iter()
.map(|trash| trash.id)
.collect::<Vec<String>>();
let mut views = match section_type {
Section::Favorite => folder.get_all_favorites(),
Section::Recent => folder.get_all_recent_sections(),
_ => vec![],
};
// filter the views that are in the trash
views.retain(|view| !trash_ids.contains(&view.id));
views
})
}
}
/// Listen on the [ViewChange] after create/delete/update events happened