Merge branch 'autocomplete-workflow' into initial-workflow

codemagic-setup
Hank Grabowski 2022-12-28 15:57:42 -05:00
commit f2fb17a3fb
12 zmienionych plików z 228 dodań i 90 usunięć

Wyświetl plik

@ -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);
});
},
),
),
],
),
);
}
}

Wyświetl plik

@ -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,
),
);
}

Wyświetl plik

@ -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;

Wyświetl plik

@ -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;

Wyświetl plik

@ -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(

Wyświetl plik

@ -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;

Wyświetl plik

@ -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,

Wyświetl plik

@ -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');

Wyświetl plik

@ -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,

Wyświetl plik

@ -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();
}

Wyświetl plik

@ -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:

Wyświetl plik

@ -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: