kopia lustrzana https://gitlab.com/mysocialportal/relatica
Merge branch 'autocomplete-workflow' into initial-workflow
commit
f2fb17a3fb
|
@ -0,0 +1,77 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../globals.dart';
|
||||
import '../../models/connection.dart';
|
||||
import '../../services/connections_manager.dart';
|
||||
|
||||
class MentionAutocompleteOptions extends StatelessWidget {
|
||||
const MentionAutocompleteOptions({
|
||||
Key? key,
|
||||
required this.query,
|
||||
required this.onMentionUserTap,
|
||||
}) : super(key: key);
|
||||
|
||||
final String query;
|
||||
final ValueSetter<Connection> onMentionUserTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final users = getIt<ConnectionsManager>().getMyContacts().where((it) {
|
||||
final normalizedHandle = it.handle.toLowerCase();
|
||||
final normalizedName = it.name.toLowerCase();
|
||||
final normalizedQuery = query.toLowerCase();
|
||||
return normalizedHandle.contains(normalizedQuery) ||
|
||||
normalizedName.contains(normalizedQuery);
|
||||
});
|
||||
|
||||
if (users.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.all(8),
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
color: const Color(0xFFF7F7F8),
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
horizontalTitleGap: 0,
|
||||
title: Text("Users matching '$query'"),
|
||||
),
|
||||
),
|
||||
LimitedBox(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.3,
|
||||
child: ListView.separated(
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: true,
|
||||
itemCount: users.length,
|
||||
separatorBuilder: (_, __) => const Divider(height: 0),
|
||||
itemBuilder: (context, i) {
|
||||
final user = users.elementAt(i);
|
||||
return ListTile(
|
||||
dense: true,
|
||||
leading: CachedNetworkImage(
|
||||
imageUrl: user.avatarUrl.toString(),
|
||||
width: 25,
|
||||
height: 25,
|
||||
),
|
||||
title: Text(user.name),
|
||||
subtitle: Text('@${user.handle}'),
|
||||
onTap: () {
|
||||
print('tapped');
|
||||
onMentionUserTap(user);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,10 +1,7 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:multi_trigger_autocomplete/multi_trigger_autocomplete.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:result_monad/result_monad.dart';
|
||||
|
||||
|
@ -26,9 +23,6 @@ void main() async {
|
|||
await dotenv.load(fileName: '.env');
|
||||
Logger.root.level = Level.FINER;
|
||||
|
||||
MultipartFile.fromBytes('file', Uint8List.fromList([]),
|
||||
filename: 'hello.jpg', contentType: MediaType('image', 'jpeg'));
|
||||
|
||||
// Logger.root.onRecord.listen((event) {
|
||||
// final logName = event.loggerName.isEmpty ? 'ROOT' : event.loggerName;
|
||||
// final msg =
|
||||
|
@ -81,40 +75,42 @@ class App extends StatelessWidget {
|
|||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<AuthService>(
|
||||
create: (_) => getIt<AuthService>(),
|
||||
lazy: true,
|
||||
return Portal(
|
||||
child: MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<AuthService>(
|
||||
create: (_) => getIt<AuthService>(),
|
||||
lazy: true,
|
||||
),
|
||||
ChangeNotifierProvider<ConnectionsManager>(
|
||||
create: (_) => getIt<ConnectionsManager>(),
|
||||
lazy: true,
|
||||
),
|
||||
ChangeNotifierProvider<EntryManagerService>(
|
||||
create: (_) => getIt<EntryManagerService>(),
|
||||
lazy: true,
|
||||
),
|
||||
ChangeNotifierProvider<GalleryService>(
|
||||
create: (_) => getIt<GalleryService>(),
|
||||
lazy: true,
|
||||
),
|
||||
ChangeNotifierProvider<TimelineManager>(
|
||||
create: (_) => getIt<TimelineManager>(),
|
||||
),
|
||||
ChangeNotifierProvider<NotificationsManager>(
|
||||
create: (_) => getIt<NotificationsManager>(),
|
||||
),
|
||||
],
|
||||
child: MaterialApp.router(
|
||||
theme: ThemeData(
|
||||
primarySwatch: Colors.indigo,
|
||||
),
|
||||
debugShowCheckedModeBanner: false,
|
||||
scrollBehavior: AppScrollingBehavior(),
|
||||
routerDelegate: appRouter.routerDelegate,
|
||||
routeInformationProvider: appRouter.routeInformationProvider,
|
||||
routeInformationParser: appRouter.routeInformationParser,
|
||||
),
|
||||
ChangeNotifierProvider<ConnectionsManager>(
|
||||
create: (_) => getIt<ConnectionsManager>(),
|
||||
lazy: true,
|
||||
),
|
||||
ChangeNotifierProvider<EntryManagerService>(
|
||||
create: (_) => getIt<EntryManagerService>(),
|
||||
lazy: true,
|
||||
),
|
||||
ChangeNotifierProvider<GalleryService>(
|
||||
create: (_) => getIt<GalleryService>(),
|
||||
lazy: true,
|
||||
),
|
||||
ChangeNotifierProvider<TimelineManager>(
|
||||
create: (_) => getIt<TimelineManager>(),
|
||||
),
|
||||
ChangeNotifierProvider<NotificationsManager>(
|
||||
create: (_) => getIt<NotificationsManager>(),
|
||||
),
|
||||
],
|
||||
child: MaterialApp.router(
|
||||
theme: ThemeData(
|
||||
primarySwatch: Colors.indigo,
|
||||
),
|
||||
debugShowCheckedModeBanner: false,
|
||||
scrollBehavior: AppScrollingBehavior(),
|
||||
routerDelegate: appRouter.routerDelegate,
|
||||
routeInformationProvider: appRouter.routeInformationProvider,
|
||||
routeInformationParser: appRouter.routeInformationParser,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ class Connection {
|
|||
|
||||
final String name;
|
||||
|
||||
final String handle;
|
||||
|
||||
final String id;
|
||||
|
||||
final Uri profileUrl;
|
||||
|
@ -11,34 +13,35 @@ class Connection {
|
|||
|
||||
final Uri avatarUrl;
|
||||
|
||||
Connection(
|
||||
{this.status = ConnectionStatus.unknown,
|
||||
this.name = '',
|
||||
this.id = '',
|
||||
Uri? profileUrl,
|
||||
this.network = '',
|
||||
Uri? avatarUrl})
|
||||
Connection({this.status = ConnectionStatus.unknown,
|
||||
this.name = '',
|
||||
this.handle = '',
|
||||
this.id = '',
|
||||
Uri? profileUrl,
|
||||
this.network = '',
|
||||
Uri? avatarUrl})
|
||||
: profileUrl = profileUrl ?? Uri(),
|
||||
avatarUrl = avatarUrl ?? Uri();
|
||||
|
||||
bool get isEmpty =>
|
||||
name.isEmpty &&
|
||||
id.isEmpty &&
|
||||
network.isEmpty &&
|
||||
status == ConnectionStatus.unknown;
|
||||
id.isEmpty &&
|
||||
network.isEmpty &&
|
||||
status == ConnectionStatus.unknown;
|
||||
|
||||
bool get isNotEmpty => !isEmpty;
|
||||
|
||||
Connection copy(
|
||||
{ConnectionStatus? status,
|
||||
String? name,
|
||||
String? id,
|
||||
Uri? profileUrl,
|
||||
String? network,
|
||||
Uri? avatarUrl}) =>
|
||||
Connection copy({ConnectionStatus? status,
|
||||
String? name,
|
||||
String? handle,
|
||||
String? id,
|
||||
Uri? profileUrl,
|
||||
String? network,
|
||||
Uri? avatarUrl}) =>
|
||||
Connection(
|
||||
status: status ?? this.status,
|
||||
name: name ?? this.name,
|
||||
handle: handle ?? this.handle,
|
||||
id: id ?? this.id,
|
||||
profileUrl: profileUrl ?? this.profileUrl,
|
||||
network: network ?? this.network,
|
||||
|
@ -47,13 +50,14 @@ class Connection {
|
|||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Connection{status: $status, name: $name, id: $id, profileUrl: $profileUrl, network: $network, avatar: $avatarUrl}';
|
||||
return 'Connection{status: $status, name: $name, id: $id, handle: $handle, profileUrl: $profileUrl, network: $network, avatar: $avatarUrl}';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is Connection && runtimeType == other.runtimeType && id == other.id;
|
||||
other is Connection && runtimeType == other.runtimeType &&
|
||||
id == other.id;
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter_portal/models/exec_error.dart';
|
||||
import 'package:result_monad/result_monad.dart';
|
||||
|
||||
import 'exec_error.dart';
|
||||
|
||||
class Credentials {
|
||||
final String username;
|
||||
final String password;
|
||||
|
|
|
@ -2,9 +2,11 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:multi_trigger_autocomplete/multi_trigger_autocomplete.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../controls/autocomplete/mention_autocomplete_options.dart';
|
||||
import '../controls/entry_media_attachments/gallery_selector_control.dart';
|
||||
import '../controls/entry_media_attachments/media_uploads_control.dart';
|
||||
import '../controls/padding.dart';
|
||||
|
@ -33,6 +35,7 @@ class _EditorScreenState extends State<EditorScreen> {
|
|||
TimelineEntry? parentEntry;
|
||||
final newMediaItems = NewEntryMediaItems();
|
||||
final existingMediaItems = <ImageEntry>[];
|
||||
final focusNode = FocusNode();
|
||||
|
||||
var isSubmitting = false;
|
||||
|
||||
|
@ -120,22 +123,7 @@ class _EditorScreenState extends State<EditorScreen> {
|
|||
),
|
||||
),
|
||||
const VerticalPadding(),
|
||||
TextFormField(
|
||||
readOnly: isSubmitting,
|
||||
enabled: !isSubmitting,
|
||||
maxLines: 10,
|
||||
controller: contentController,
|
||||
decoration: InputDecoration(
|
||||
labelText: '$statusType Content',
|
||||
alignLabelWithHint: true,
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).backgroundColor,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
buildContentField(context),
|
||||
const VerticalPadding(),
|
||||
GallerySelectorControl(entries: existingMediaItems),
|
||||
const VerticalPadding(),
|
||||
|
@ -181,6 +169,45 @@ class _EditorScreenState extends State<EditorScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget buildContentField(BuildContext context) {
|
||||
return MultiTriggerAutocomplete(
|
||||
textEditingController: contentController,
|
||||
focusNode: focusNode,
|
||||
optionsAlignment: OptionsAlignment.bottomEnd,
|
||||
autocompleteTriggers: [
|
||||
AutocompleteTrigger(
|
||||
trigger: '@',
|
||||
optionsViewBuilder: (context, autocompleteQuery, controller) {
|
||||
return MentionAutocompleteOptions(
|
||||
query: autocompleteQuery.query,
|
||||
onMentionUserTap: (user) {
|
||||
final autocomplete = MultiTriggerAutocomplete.of(context);
|
||||
return autocomplete.acceptAutocompleteOption(user.handle);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
fieldViewBuilder: (context, controller, focusNode) => TextFormField(
|
||||
focusNode: focusNode,
|
||||
readOnly: isSubmitting,
|
||||
enabled: !isSubmitting,
|
||||
maxLines: 10,
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
labelText: '$statusType Content',
|
||||
alignLabelWithHint: true,
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).backgroundColor,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildCommentPreview(BuildContext context, TimelineEntry entry) {
|
||||
_logger.finest('Build preview');
|
||||
return Column(
|
||||
|
|
|
@ -4,13 +4,13 @@ import 'package:cached_network_image/cached_network_image.dart';
|
|||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
|
||||
import 'package:flutter_portal/utils/snackbar_builder.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../globals.dart';
|
||||
import '../models/media_attachment.dart';
|
||||
import '../services/auth_service.dart';
|
||||
import '../utils/snackbar_builder.dart';
|
||||
|
||||
class ImageViewerScreen extends StatelessWidget {
|
||||
final MediaAttachment attachment;
|
||||
|
|
|
@ -57,17 +57,18 @@ class _UserProfileScreenState extends State<UserProfileScreen> {
|
|||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
notMyProfile
|
||||
? profile.name
|
||||
: '${profile.name} (Your Account)',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const HorizontalPadding(),
|
||||
Text('( ${profile.status.label()} )'),
|
||||
],
|
||||
children: [],
|
||||
),
|
||||
Text(
|
||||
notMyProfile
|
||||
? '${profile.name} (${profile.handle})'
|
||||
: '${profile.name} (Your Account)',
|
||||
softWrap: true,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const VerticalPadding(),
|
||||
Text('( ${profile.status.label()} )'),
|
||||
const VerticalPadding(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import 'package:flutter_portal/serializers/friendica/connection_friendica_extensions.dart';
|
||||
import 'package:flutter_portal/serializers/friendica/media_attachment_friendica_extensions.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import '../../models/location_data.dart';
|
||||
import '../../models/timeline_entry.dart';
|
||||
import '../../utils/dateutils.dart';
|
||||
import 'connection_friendica_extensions.dart';
|
||||
import 'media_attachment_friendica_extensions.dart';
|
||||
|
||||
final _logger = Logger('FriendicaTimelineEntrySerializer');
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import '../../globals.dart';
|
||||
import '../../models/connection.dart';
|
||||
import '../../services/auth_service.dart';
|
||||
|
||||
extension ConnectionMastodonExtensions on Connection {
|
||||
static Connection fromJson(Map<String, dynamic> json) {
|
||||
|
@ -7,10 +9,20 @@ extension ConnectionMastodonExtensions on Connection {
|
|||
final profileUrl = Uri.parse(json['url'] ?? '');
|
||||
const network = 'Unknown';
|
||||
final avatar = Uri.tryParse(json['avatar_static'] ?? '') ?? Uri();
|
||||
final String handleFromJson = json['acct'];
|
||||
|
||||
late final String handle;
|
||||
if (handleFromJson.contains('@')) {
|
||||
handle = handleFromJson;
|
||||
} else {
|
||||
final server = getIt<AuthService>().currentServer;
|
||||
handle = '$handleFromJson@$server';
|
||||
}
|
||||
|
||||
return Connection(
|
||||
name: name,
|
||||
id: id,
|
||||
handle: handle,
|
||||
profileUrl: profileUrl,
|
||||
network: network,
|
||||
avatarUrl: avatar,
|
||||
|
|
|
@ -13,6 +13,7 @@ class AuthService extends ChangeNotifier {
|
|||
FriendicaClient? _friendicaClient;
|
||||
String _currentId = '';
|
||||
String _currentHandle = '';
|
||||
String _currentServer = '';
|
||||
bool _loggedIn = false;
|
||||
|
||||
Result<FriendicaClient, ExecError> get currentClient {
|
||||
|
@ -32,6 +33,8 @@ class AuthService extends ChangeNotifier {
|
|||
|
||||
String get currentHandle => _currentHandle;
|
||||
|
||||
String get currentServer => _currentServer;
|
||||
|
||||
Future<bool> getStoredLoginState() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool('logged-in') ?? false;
|
||||
|
@ -50,6 +53,7 @@ class AuthService extends ChangeNotifier {
|
|||
_friendicaClient = client;
|
||||
_currentId = result.value.id;
|
||||
_currentHandle = client.credentials.handle;
|
||||
_currentServer = client.credentials.serverName;
|
||||
notifyListeners();
|
||||
return Result.ok(client);
|
||||
}
|
||||
|
@ -61,6 +65,7 @@ class AuthService extends ChangeNotifier {
|
|||
_friendicaClient = null;
|
||||
_currentId = '';
|
||||
_currentHandle = '';
|
||||
_currentServer = '';
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
|
|
14
pubspec.lock
14
pubspec.lock
|
@ -195,6 +195,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.7"
|
||||
flutter_portal:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_portal
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.3"
|
||||
flutter_secure_storage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -408,6 +415,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.1"
|
||||
multi_trigger_autocomplete:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: multi_trigger_autocomplete
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.1"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
name: flutter_portal
|
||||
name: friendica_portal
|
||||
description: Friendica Social Network Application
|
||||
|
||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
|
@ -35,6 +35,7 @@ dependencies:
|
|||
path: ^1.8.2
|
||||
image: ^3.2.2
|
||||
flutter_file_dialog: ^2.3.2
|
||||
multi_trigger_autocomplete: ^0.1.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
Ładowanie…
Reference in New Issue