diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index b30b33d..86f0f95 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -2,6 +2,8 @@ PODS:
- Flutter (1.0.0)
- flutter_secure_storage (6.0.0):
- Flutter
+ - image_picker_ios (0.0.1):
+ - Flutter
- media_kit_video (0.0.1):
- Flutter
- package_info_plus (0.4.5):
@@ -26,6 +28,7 @@ PODS:
DEPENDENCIES:
- Flutter (from `Flutter`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
+ - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
@@ -41,6 +44,8 @@ EXTERNAL SOURCES:
:path: Flutter
flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
+ image_picker_ios:
+ :path: ".symlinks/plugins/image_picker_ios/ios"
media_kit_video:
:path: ".symlinks/plugins/media_kit_video/ios"
package_info_plus:
@@ -63,6 +68,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
+ image_picker_ios: b545a5f16c0fa88e3ecbbce3ed4de45567a8ec18
media_kit_video: 26c5b265a4094a2df3e8d41e6724d9b964c13151
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 4af3b37..bb54496 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -35,6 +35,12 @@
UILaunchStoryboardName
LaunchScreen
+ NSPhotoLibraryUsageDescription
+ Allow you add photo to your message or post
+ NSCameraUsageDescription
+ Allow you take photo/video for your message or post
+ NSMicrophoneUsageDescription
+ Allow you record audio for your message or post
UIMainStoryboardFile
Main
UISupportedInterfaceOrientations
diff --git a/lib/i18n/app_en.arb b/lib/i18n/app_en.arb
index eff1fd3..7fb89e9 100644
--- a/lib/i18n/app_en.arb
+++ b/lib/i18n/app_en.arb
@@ -7,5 +7,12 @@
"signUp": "Sign Up",
"signUpCaption": "Create an account on Solarpass and then get the access of entire Solar Networks!",
"post": "Post",
- "comment": "Comment"
-}
\ No newline at end of file
+ "postVerb": "Post",
+ "comment": "Comment",
+ "attachment": "Attachment",
+ "attachmentAdd": "Add new attachment",
+ "pickPhoto": "Gallery photo",
+ "newMoment": "Record a moment",
+ "postIdentityNotify": "You will create this post as",
+ "postContentPlaceholder": "What's happened?!"
+}
diff --git a/lib/i18n/app_zh.arb b/lib/i18n/app_zh.arb
index 7fe6c15..348e1f4 100644
--- a/lib/i18n/app_zh.arb
+++ b/lib/i18n/app_zh.arb
@@ -7,5 +7,12 @@
"signUp": "注册",
"signUpCaption": "在 Solarpass 注册一个账号以获得整个 Solar Networks 的存取权!",
"post": "帖子",
- "comment": "评论"
+ "postVerb": "发表",
+ "comment": "评论",
+ "attachment": "附件",
+ "attachmentAdd": "附加新附件",
+ "pickPhoto": "相册照片",
+ "newMoment": "记录时刻",
+ "postIdentityNotify": "你将会以该身份发表本帖子",
+ "postContentPlaceholder": "发生什么事了?!"
}
\ No newline at end of file
diff --git a/lib/providers/auth.dart b/lib/providers/auth.dart
index 753ed9a..39e86a2 100755
--- a/lib/providers/auth.dart
+++ b/lib/providers/auth.dart
@@ -7,10 +7,10 @@ import 'package:solian/screens/auth.dart';
import 'package:oauth2/oauth2.dart' as oauth2;
import 'package:solian/utils/service_url.dart';
-final authClient = AuthProvider();
-
class AuthProvider {
- AuthProvider();
+ AuthProvider() {
+ pickClient();
+ }
final deviceEndpoint =
getRequestUri('passport', '/api/notifications/subscribe');
@@ -61,7 +61,8 @@ class AuthProvider {
basicAuth: false,
);
- var authorizationUrl = grant.getAuthorizationUrl(redirectUrl, scopes: ["openid"]);
+ var authorizationUrl =
+ grant.getAuthorizationUrl(redirectUrl, scopes: ["openid"]);
if (Platform.isAndroid || Platform.isIOS) {
// Use WebView to get authorization url
diff --git a/lib/screens/account.dart b/lib/screens/account.dart
index fa51be5..e717123 100644
--- a/lib/screens/account.dart
+++ b/lib/screens/account.dart
@@ -3,7 +3,7 @@ import 'package:provider/provider.dart';
import 'package:solian/providers/auth.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:solian/utils/service_url.dart';
-import 'package:solian/widgets/wrapper.dart';
+import 'package:solian/widgets/common_wrapper.dart';
import 'package:url_launcher/url_launcher.dart';
class AccountScreen extends StatefulWidget {
@@ -66,7 +66,7 @@ class _AccountScreenState extends State {
caption: AppLocalizations.of(context)!.signInCaption,
onTap: () {
auth.signIn(context).then((_) {
- authClient.isAuthorized().then((val) {
+ auth.isAuthorized().then((val) {
setState(() => isAuthorized = val);
});
});
@@ -90,13 +90,15 @@ class _AccountScreenState extends State {
class NameCard extends StatelessWidget {
const NameCard({super.key});
- Future renderAvatar() async {
- final profiles = await authClient.getProfiles();
+ Future renderAvatar(BuildContext context) async {
+ final auth = context.read();
+ final profiles = await auth.getProfiles();
return CircleAvatar(backgroundImage: NetworkImage(profiles["picture"]));
}
- Future renderLabel() async {
- final profiles = await authClient.getProfiles();
+ Future renderLabel(BuildContext context) async {
+ final auth = context.read();
+ final profiles = await auth.getProfiles();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -122,7 +124,7 @@ class NameCard extends StatelessWidget {
child: Row(
children: [
FutureBuilder(
- future: renderAvatar(),
+ future: renderAvatar(context),
builder:
(BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
@@ -134,7 +136,7 @@ class NameCard extends StatelessWidget {
),
const SizedBox(width: 20),
FutureBuilder(
- future: renderLabel(),
+ future: renderLabel(context),
builder:
(BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart
index 16e0d8d..d4a7baf 100644
--- a/lib/screens/explore.dart
+++ b/lib/screens/explore.dart
@@ -8,8 +8,8 @@ import 'package:solian/utils/service_url.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:http/http.dart' as http;
+import 'package:solian/widgets/indent_wrapper.dart';
import 'package:solian/widgets/posts/item.dart';
-import 'package:solian/widgets/wrapper.dart';
class ExploreScreen extends StatefulWidget {
const ExploreScreen({super.key});
@@ -58,7 +58,15 @@ class _ExploreScreenState extends State {
@override
Widget build(BuildContext context) {
- return LayoutWrapper(
+ return IndentWrapper(
+ noSafeArea: true,
+ floatingActionButton: FloatingActionButton(
+ child: const Icon(Icons.edit),
+ onPressed: () async {
+ final did = await router.pushNamed("posts.moments.new");
+ if (did == true) _pagingController.refresh();
+ },
+ ),
title: AppLocalizations.of(context)!.explore,
child: RefreshIndicator(
onRefresh: () => Future.sync(
@@ -75,7 +83,7 @@ class _ExploreScreenState extends State {
itemBuilder: (context, item, index) => GestureDetector(
child: PostItem(item: item),
onTap: () {
- router.goNamed(
+ router.pushNamed(
'posts.screen',
pathParameters: {
'alias': item.alias,
diff --git a/lib/screens/posts/new_moment.dart b/lib/screens/posts/new_moment.dart
index 9475789..87b6fe6 100644
--- a/lib/screens/posts/new_moment.dart
+++ b/lib/screens/posts/new_moment.dart
@@ -1,19 +1,143 @@
-import 'package:flutter/material.dart';
+import 'dart:convert';
-class NewMomentScreen extends StatelessWidget {
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import 'package:solian/models/post.dart';
+import 'package:solian/providers/auth.dart';
+import 'package:solian/router.dart';
+import 'package:solian/utils/service_url.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:solian/widgets/indent_wrapper.dart';
+import 'package:solian/widgets/posts/attachment_editor.dart';
+
+class NewMomentScreen extends StatefulWidget {
const NewMomentScreen({super.key});
@override
- Widget build(BuildContext context) {
- return Center(
- child: Container(
- constraints: const BoxConstraints(maxWidth: 640),
- child: Column(
- children: [
+ State createState() => _NewMomentScreenState();
+}
- ],
+class _NewMomentScreenState extends State {
+ final _textController = TextEditingController();
+
+ bool _isSubmitting = false;
+
+ List _attachments = List.empty(growable: true);
+
+ void viewAttachments(BuildContext context) {
+ showModalBottomSheet(
+ context: context,
+ builder: (context) => AttachmentEditor(
+ current: _attachments,
+ onUpdate: (value) => _attachments = value,
+ ),
+ );
+ }
+
+ Future createPost(BuildContext context) async {
+ final auth = context.read();
+ if (!await auth.isAuthorized()) return;
+
+ setState(() => _isSubmitting = true);
+ var res = await auth.client!.post(
+ getRequestUri('interactive', '/api/p/moments'),
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: jsonEncode({
+ 'content': _textController.value.text,
+ 'attachments': _attachments,
+ }),
+ );
+ if (res.statusCode != 200) {
+ var message = utf8.decode(res.bodyBytes);
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text("Something went wrong... $message")),
+ );
+ } else {
+ if (router.canPop()) {
+ router.pop(true);
+ }
+ }
+ setState(() => _isSubmitting = false);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final auth = context.read();
+
+ return IndentWrapper(
+ hideDrawer: true,
+ title: AppLocalizations.of(context)!.newMoment,
+ appBarActions: [
+ TextButton(
+ onPressed: !_isSubmitting ? () => createPost(context) : null,
+ child: Text(AppLocalizations.of(context)!.postVerb),
+ ),
+ ],
+ child: Center(
+ child: Container(
+ constraints: const BoxConstraints(maxWidth: 640),
+ child: Column(
+ children: [
+ _isSubmitting ? const LinearProgressIndicator() : Container(),
+ FutureBuilder(
+ future: auth.getProfiles(),
+ builder: (context, snapshot) {
+ if (snapshot.hasData) {
+ var userinfo = snapshot.data;
+ return ListTile(
+ title: Text(userinfo["nick"]),
+ subtitle: Text(
+ AppLocalizations.of(context)!.postIdentityNotify,
+ ),
+ leading: CircleAvatar(
+ backgroundImage: NetworkImage(userinfo["picture"]),
+ ),
+ );
+ } else {
+ return Container();
+ }
+ },
+ ),
+ const Divider(thickness: 0.3),
+ Expanded(
+ child: Container(
+ padding:
+ const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ child: TextField(
+ maxLines: null,
+ autofocus: true,
+ autocorrect: true,
+ keyboardType: TextInputType.multiline,
+ controller: _textController,
+ decoration: InputDecoration.collapsed(
+ hintText:
+ AppLocalizations.of(context)!.postContentPlaceholder,
+ ),
+ ),
+ ),
+ ),
+ Container(
+ decoration: const BoxDecoration(
+ border: Border(
+ top: BorderSide(width: 0.3, color: Color(0xffdedede)),
+ ),
+ ),
+ child: Row(
+ children: [
+ TextButton(
+ style: TextButton.styleFrom(shape: const CircleBorder()),
+ child: const Icon(Icons.camera_alt),
+ onPressed: () => viewAttachments(context),
+ )
+ ],
+ ),
+ ),
+ ],
+ ),
),
),
);
}
-}
\ No newline at end of file
+}
diff --git a/lib/screens/posts/screen.dart b/lib/screens/posts/screen.dart
index e874911..543dbba 100644
--- a/lib/screens/posts/screen.dart
+++ b/lib/screens/posts/screen.dart
@@ -5,9 +5,9 @@ import 'package:http/http.dart' as http;
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:solian/models/post.dart';
import 'package:solian/utils/service_url.dart';
+import 'package:solian/widgets/indent_wrapper.dart';
import 'package:solian/widgets/posts/comment_list.dart';
import 'package:solian/widgets/posts/item.dart';
-import 'package:solian/widgets/wrapper.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class PostScreen extends StatefulWidget {
@@ -43,7 +43,8 @@ class _PostScreenState extends State {
@override
Widget build(BuildContext context) {
- return LayoutWrapper(
+ return IndentWrapper(
+ hideDrawer: true,
title: AppLocalizations.of(context)!.post,
child: FutureBuilder(
future: fetchPost(context),
diff --git a/lib/widgets/wrapper.dart b/lib/widgets/common_wrapper.dart
similarity index 86%
rename from lib/widgets/wrapper.dart
rename to lib/widgets/common_wrapper.dart
index 3befc6e..7dd12dc 100644
--- a/lib/widgets/wrapper.dart
+++ b/lib/widgets/common_wrapper.dart
@@ -12,7 +12,9 @@ class LayoutWrapper extends StatelessWidget {
return Scaffold(
drawer: const SolianNavigationDrawer(),
appBar: AppBar(title: Text(title)),
- body: child ?? Container(),
+ body: SafeArea(
+ child: child ?? Container(),
+ ),
);
}
}
diff --git a/lib/widgets/indent_wrapper.dart b/lib/widgets/indent_wrapper.dart
new file mode 100644
index 0000000..4029f2e
--- /dev/null
+++ b/lib/widgets/indent_wrapper.dart
@@ -0,0 +1,25 @@
+import 'package:flutter/material.dart';
+import 'package:solian/widgets/navigation_drawer.dart';
+
+class IndentWrapper extends StatelessWidget {
+ final Widget? child;
+ final Widget? floatingActionButton;
+ final List? appBarActions;
+ final bool? hideDrawer;
+ final bool? noSafeArea;
+ final String title;
+
+ const IndentWrapper({super.key, this.child, required this.title, this.floatingActionButton, this.appBarActions, this.hideDrawer, this.noSafeArea});
+
+ @override
+ Widget build(BuildContext context) {
+ final content = child ?? Container();
+
+ return Scaffold(
+ appBar: AppBar(title: Text(title), actions: appBarActions),
+ floatingActionButton: floatingActionButton,
+ drawer: (hideDrawer ?? false) ? null : const SolianNavigationDrawer(),
+ body: (noSafeArea ?? false) ? content : SafeArea(child: content),
+ );
+ }
+}
diff --git a/lib/widgets/posts/attachment_editor.dart b/lib/widgets/posts/attachment_editor.dart
new file mode 100755
index 0000000..7d9ca1e
--- /dev/null
+++ b/lib/widgets/posts/attachment_editor.dart
@@ -0,0 +1,300 @@
+import 'dart:convert';
+import 'dart:io';
+import 'dart:math' as math;
+
+import 'package:crypto/crypto.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:http/http.dart';
+import 'package:image_picker/image_picker.dart';
+import 'package:provider/provider.dart';
+import 'package:solian/models/post.dart';
+import 'package:solian/providers/auth.dart';
+import 'package:solian/utils/service_url.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+
+class AttachmentEditor extends StatefulWidget {
+ final List current;
+ final void Function(List data) onUpdate;
+
+ const AttachmentEditor(
+ {super.key, required this.current, required this.onUpdate});
+
+ @override
+ State createState() => _AttachmentEditorState();
+}
+
+class _AttachmentEditorState extends State {
+ final _imagePicker = ImagePicker();
+
+ bool isSubmitting = false;
+
+ List _attachments = List.empty(growable: true);
+
+ void viewAttachMethods(BuildContext context) {
+ showModalBottomSheet(
+ context: context,
+ builder: (context) => AttachmentEditorMethodPopup(
+ pickImage: () => pickImageToUpload(context),
+ ),
+ );
+ }
+
+ Future pickImageToUpload(BuildContext context) async {
+ final auth = context.read();
+ if (!await auth.isAuthorized()) return;
+
+ final image = await _imagePicker.pickImage(source: ImageSource.gallery);
+ if (image == null) return;
+
+ setState(() => isSubmitting = true);
+
+ final file = File(image.path);
+ final hashcode = await calculateSha256(file);
+
+ if (Navigator.canPop(context)) {
+ Navigator.pop(context);
+ }
+
+ try {
+ await uploadAttachment(file, hashcode);
+ } catch (err) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text("Something went wrong... $err")),
+ );
+ } finally {
+ setState(() => isSubmitting = false);
+ }
+ }
+
+ Future uploadAttachment(File file, String hashcode) async {
+ final auth = context.read();
+ final req = MultipartRequest(
+ 'POST',
+ getRequestUri('interactive', '/api/attachments'),
+ );
+ req.files.add(await MultipartFile.fromPath('attachment', file.path));
+ req.fields['hashcode'] = hashcode;
+
+ var res = await auth.client!.send(req);
+ print(res);
+ if (res.statusCode == 200) {
+ var result = Attachment.fromJson(
+ jsonDecode(utf8.decode(await res.stream.toBytes()))["info"],
+ );
+ setState(() => _attachments.add(result));
+ widget.onUpdate(_attachments);
+ } else {
+ throw Exception(utf8.decode(await res.stream.toBytes()));
+ }
+ }
+
+ Future disposeAttachment(
+ BuildContext context, Attachment item, int index) async {
+ final auth = context.read();
+
+ final req = MultipartRequest(
+ 'DELETE',
+ getRequestUri('interactive', '/api/attachments/${item.id}'),
+ );
+
+ setState(() => isSubmitting = true);
+ var res = await auth.client!.send(req);
+ if (res.statusCode == 200) {
+ setState(() => _attachments.removeAt(index));
+ widget.onUpdate(_attachments);
+ } else {
+ final err = utf8.decode(await res.stream.toBytes());
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text("Something went wrong... $err")),
+ );
+ }
+ setState(() => isSubmitting = false);
+ }
+
+ Future calculateSha256(File file) async {
+ final bytes = await file.readAsBytes();
+ final digest = sha256.convert(bytes);
+ return digest.toString();
+ }
+
+ String getFileName(Attachment item) {
+ return item.filename.replaceAll(RegExp(r'\.[^/.]+$'), '');
+ }
+
+ String getFileType(Attachment item) {
+ switch (item.type) {
+ case 1:
+ return 'Photo';
+ case 2:
+ return 'Video';
+ case 3:
+ return 'Audio';
+ default:
+ return 'Others';
+ }
+ }
+
+ String formatBytes(int bytes, {int decimals = 2}) {
+ if (bytes == 0) return '0 Bytes';
+ const k = 1024;
+ final dm = decimals < 0 ? 0 : decimals;
+ final sizes = [
+ 'Bytes',
+ 'KiB',
+ 'MiB',
+ 'GiB',
+ 'TiB',
+ 'PiB',
+ 'EiB',
+ 'ZiB',
+ 'YiB'
+ ];
+ final i = (math.log(bytes) / math.log(k)).floor().toInt();
+ return '${(bytes / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}';
+ }
+
+ @override
+ void initState() {
+ _attachments = widget.current;
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final auth = context.read();
+
+ return Column(
+ children: [
+ Container(
+ padding: const EdgeInsets.only(left: 8, right: 8, top: 20),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 8.0,
+ vertical: 12.0,
+ ),
+ child: Text(
+ AppLocalizations.of(context)!.attachment,
+ style: Theme.of(context).textTheme.headlineSmall,
+ ),
+ ),
+ FutureBuilder(
+ future: auth.isAuthorized(),
+ builder: (context, snapshot) {
+ if (snapshot.hasData && snapshot.data == true) {
+ return TextButton(
+ onPressed: isSubmitting
+ ? null
+ : () => viewAttachMethods(context),
+ style: TextButton.styleFrom(shape: const CircleBorder()),
+ child: const Icon(Icons.add_circle),
+ );
+ } else {
+ return Container();
+ }
+ },
+ ),
+ ],
+ ),
+ ),
+ isSubmitting ? const LinearProgressIndicator() : Container(),
+ Expanded(
+ child: ListView.separated(
+ itemCount: _attachments.length,
+ itemBuilder: (context, index) {
+ var element = _attachments[index];
+ return Container(
+ padding: const EdgeInsets.only(left: 16, right: 8),
+ child: Row(
+ children: [
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ getFileName(element),
+ overflow: TextOverflow.ellipsis,
+ maxLines: 1,
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ Text(
+ "${getFileType(element)} · ${formatBytes(element.filesize)}",
+ ),
+ ],
+ ),
+ ),
+ TextButton(
+ style: TextButton.styleFrom(
+ shape: const CircleBorder(),
+ foregroundColor: Colors.red,
+ ),
+ child: const Icon(Icons.delete),
+ onPressed: () =>
+ disposeAttachment(context, element, index),
+ ),
+ ],
+ ),
+ );
+ },
+ separatorBuilder: (context, index) => const Divider(),
+ ),
+ ),
+ ],
+ );
+ }
+}
+
+class AttachmentEditorMethodPopup extends StatelessWidget {
+ final Function pickImage;
+
+ const AttachmentEditorMethodPopup({super.key, required this.pickImage});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ height: 320,
+ padding: const EdgeInsets.only(left: 8, right: 8, top: 20),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 8.0,
+ vertical: 12.0,
+ ),
+ child: Text(
+ AppLocalizations.of(context)!.attachmentAdd,
+ style: Theme.of(context).textTheme.headlineSmall,
+ ),
+ ),
+ Expanded(
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: [
+ InkWell(
+ borderRadius: BorderRadius.circular(8),
+ onTap: () => pickImage(),
+ child: Padding(
+ padding: const EdgeInsets.all(8),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const Icon(Icons.add_photo_alternate,
+ color: Colors.indigo),
+ const SizedBox(height: 8),
+ Text(AppLocalizations.of(context)!.pickPhoto),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/widgets/posts/item.dart b/lib/widgets/posts/item.dart
index 816b21b..8b9c0a7 100644
--- a/lib/widgets/posts/item.dart
+++ b/lib/widgets/posts/item.dart
@@ -80,7 +80,7 @@ class _PostItemState extends State {
children: [
...headingParts,
Padding(
- padding: const EdgeInsets.symmetric(horizontal: 12),
+ padding: const EdgeInsets.only(left: 12, right: 12, top: 4),
child: renderContent(),
),
renderAttachments(),
diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc
index 71fe128..11c48a5 100644
--- a/linux/flutter/generated_plugin_registrant.cc
+++ b/linux/flutter/generated_plugin_registrant.cc
@@ -6,11 +6,15 @@
#include "generated_plugin_registrant.h"
+#include
#include
#include
#include
void fl_register_plugins(FlPluginRegistry* registry) {
+ g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
+ fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
+ file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake
index 4ab696d..ba9d398 100644
--- a/linux/flutter/generated_plugins.cmake
+++ b/linux/flutter/generated_plugins.cmake
@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
+ file_selector_linux
flutter_secure_storage_linux
media_kit_video
url_launcher_linux
diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift
index 0e54e39..e566c73 100644
--- a/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -5,6 +5,7 @@
import FlutterMacOS
import Foundation
+import file_selector_macos
import flutter_secure_storage_macos
import media_kit_video
import package_info_plus
@@ -15,6 +16,7 @@ import video_player_avfoundation
import wakelock_plus
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
+ FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
diff --git a/pubspec.lock b/pubspec.lock
index f01a978..ce3494c 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -73,8 +73,16 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.1"
- crypto:
+ cross_file:
dependency: transitive
+ description:
+ name: cross_file
+ sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.3.4+1"
+ crypto:
+ dependency: "direct main"
description:
name: crypto
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
@@ -121,6 +129,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.2"
+ file_selector_linux:
+ dependency: transitive
+ description:
+ name: file_selector_linux
+ sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.9.2+1"
+ file_selector_macos:
+ dependency: transitive
+ description:
+ name: file_selector_macos
+ sha256: b15c3da8bd4908b9918111fa486903f5808e388b8d1c559949f584725a6594d6
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.9.3+3"
+ file_selector_platform_interface:
+ dependency: transitive
+ description:
+ name: file_selector_platform_interface
+ sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.6.2"
+ file_selector_windows:
+ dependency: transitive
+ description:
+ name: file_selector_windows
+ sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.9.3+1"
fixnum:
dependency: transitive
description:
@@ -163,6 +203,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.22+1"
+ flutter_plugin_android_lifecycle:
+ dependency: transitive
+ description:
+ name: flutter_plugin_android_lifecycle
+ sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.19"
flutter_secure_storage:
dependency: "direct main"
description:
@@ -269,6 +317,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.7"
+ image_picker:
+ dependency: "direct main"
+ description:
+ name: image_picker
+ sha256: "1f498d086203360cca099d20ffea2963f48c39ce91bdd8a3b6d4a045786b02c8"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.8"
+ image_picker_android:
+ dependency: transitive
+ description:
+ name: image_picker_android
+ sha256: "844c6da4e4f2829dffdab97816bca09d0e0977e8dcef7450864aba4e07967a58"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.8.9+6"
+ image_picker_for_web:
+ dependency: transitive
+ description:
+ name: image_picker_for_web
+ sha256: "6a1704fdd75022272e7e7a897a9068e9c2ff3cd6a66820bf3ded810633eac954"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.3"
+ image_picker_ios:
+ dependency: transitive
+ description:
+ name: image_picker_ios
+ sha256: "917a5cadd67d052554cfb258595e54217de53fac5b52939426e26319a02e6297"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.8.9+2"
+ image_picker_linux:
+ dependency: transitive
+ description:
+ name: image_picker_linux
+ sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.1+1"
+ image_picker_macos:
+ dependency: transitive
+ description:
+ name: image_picker_macos
+ sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.1+1"
+ image_picker_platform_interface:
+ dependency: transitive
+ description:
+ name: image_picker_platform_interface
+ sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.10.0"
+ image_picker_windows:
+ dependency: transitive
+ description:
+ name: image_picker_windows
+ sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.1+1"
infinite_scroll_pagination:
dependency: "direct main"
description:
@@ -381,6 +493,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.11.0"
+ mime:
+ dependency: transitive
+ description:
+ name: mime
+ sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.5"
nested:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 9a35d62..75e6ded 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -53,6 +53,8 @@ dependencies:
flutter_secure_storage: ^9.0.0
oauth2: ^2.0.2
webview_flutter: ^4.7.0
+ crypto: ^3.0.3
+ image_picker: ^1.0.8
dev_dependencies:
flutter_test:
diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc
index c38fbf3..bfc7d59 100644
--- a/windows/flutter/generated_plugin_registrant.cc
+++ b/windows/flutter/generated_plugin_registrant.cc
@@ -6,12 +6,15 @@
#include "generated_plugin_registrant.h"
+#include
#include
#include
#include
#include
void RegisterPlugins(flutter::PluginRegistry* registry) {
+ FileSelectorWindowsRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("FileSelectorWindows"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
MediaKitVideoPluginCApiRegisterWithRegistrar(
diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake
index 827eb9c..d932f37 100644
--- a/windows/flutter/generated_plugins.cmake
+++ b/windows/flutter/generated_plugins.cmake
@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
+ file_selector_windows
flutter_secure_storage_windows
media_kit_video
screen_brightness_windows