Compare commits
No commits in common. "b70d3795d1bef964043b90f8ace03898d0aee381" and "19751617cb77638e0aea9dcbb1b013408fa8f35c" have entirely different histories.
b70d3795d1
...
19751617cb
@ -21,7 +21,7 @@
|
|||||||
<application
|
<application
|
||||||
android:label="Solian"
|
android:label="Solian"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/launcher_icon"
|
||||||
android:supportsRtl="true">
|
android:supportsRtl="true">
|
||||||
<receiver android:exported="false"
|
<receiver android:exported="false"
|
||||||
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver"/>
|
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver"/>
|
||||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 544 B |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 442 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 721 B |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 574 KiB |
BIN
assets/icon.png
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 62 KiB |
BIN
assets/logo.png
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 80 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 541 B After Width: | Height: | Size: 641 B |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 815 B After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 8.0 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 7.4 KiB |
@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
@ -102,9 +101,7 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
|||||||
for (var idx = 0; idx < _periods.length; idx++) {
|
for (var idx = 0; idx < _periods.length; idx++) {
|
||||||
await _periods[idx].action();
|
await _periods[idx].action();
|
||||||
if (_isErrored) break;
|
if (_isErrored) break;
|
||||||
if (_periodCursor < _periods.length - 1) {
|
setState(() => _periodCursor++);
|
||||||
setState(() => _periodCursor++);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setState(() => _isBusy = false);
|
setState(() => _isBusy = false);
|
||||||
@ -130,9 +127,7 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
|||||||
height: 280,
|
height: 280,
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.bottomCenter,
|
alignment: Alignment.bottomCenter,
|
||||||
child: Image.asset('assets/logo.png', width: 80, height: 80)
|
child: Image.asset('assets/logo.png', width: 80, height: 80),
|
||||||
.animate(onPlay: (c) => c.repeat())
|
|
||||||
.rotate(duration: 850.ms, curve: Curves.easeInOut),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
|
@ -6,9 +6,9 @@ import 'package:get/get.dart';
|
|||||||
import 'package:solian/models/post.dart';
|
import 'package:solian/models/post.dart';
|
||||||
import 'package:solian/models/realm.dart';
|
import 'package:solian/models/realm.dart';
|
||||||
import 'package:solian/widgets/attachments/attachment_editor.dart';
|
import 'package:solian/widgets/attachments/attachment_editor.dart';
|
||||||
import 'package:solian/widgets/posts/editor/post_editor_categories_tags.dart';
|
|
||||||
import 'package:solian/widgets/posts/editor/post_editor_overview.dart';
|
import 'package:solian/widgets/posts/editor/post_editor_overview.dart';
|
||||||
import 'package:solian/widgets/posts/editor/post_editor_visibility.dart';
|
import 'package:solian/widgets/posts/editor/post_editor_visibility.dart';
|
||||||
|
import 'package:textfield_tags/textfield_tags.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
class PostEditorController extends GetxController {
|
class PostEditorController extends GetxController {
|
||||||
@ -17,6 +17,7 @@ class PostEditorController extends GetxController {
|
|||||||
final titleController = TextEditingController();
|
final titleController = TextEditingController();
|
||||||
final descriptionController = TextEditingController();
|
final descriptionController = TextEditingController();
|
||||||
final contentController = TextEditingController();
|
final contentController = TextEditingController();
|
||||||
|
final tagController = StringTagController();
|
||||||
|
|
||||||
RxInt mode = 0.obs;
|
RxInt mode = 0.obs;
|
||||||
RxInt contentLength = 0.obs;
|
RxInt contentLength = 0.obs;
|
||||||
@ -26,7 +27,6 @@ class PostEditorController extends GetxController {
|
|||||||
Rx<Post?> repostTo = Rx(null);
|
Rx<Post?> repostTo = Rx(null);
|
||||||
Rx<Realm?> realmZone = Rx(null);
|
Rx<Realm?> realmZone = Rx(null);
|
||||||
RxList<int> attachments = RxList<int>.empty(growable: true);
|
RxList<int> attachments = RxList<int>.empty(growable: true);
|
||||||
RxList<String> tags = RxList<String>.empty(growable: true);
|
|
||||||
|
|
||||||
RxList<int> visibleUsers = RxList.empty(growable: true);
|
RxList<int> visibleUsers = RxList.empty(growable: true);
|
||||||
RxList<int> invisibleUsers = RxList.empty(growable: true);
|
RxList<int> invisibleUsers = RxList.empty(growable: true);
|
||||||
@ -79,15 +79,6 @@ class PostEditorController extends GetxController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> editCategoriesAndTags(BuildContext context) {
|
|
||||||
return showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => PostEditorCategoriesDialog(
|
|
||||||
controller: this,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> editAttachment(BuildContext context) {
|
Future<void> editAttachment(BuildContext context) {
|
||||||
return showModalBottomSheet(
|
return showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
@ -136,8 +127,8 @@ class PostEditorController extends GetxController {
|
|||||||
titleController.clear();
|
titleController.clear();
|
||||||
descriptionController.clear();
|
descriptionController.clear();
|
||||||
contentController.clear();
|
contentController.clear();
|
||||||
|
tagController.clearTags();
|
||||||
attachments.clear();
|
attachments.clear();
|
||||||
tags.clear();
|
|
||||||
visibleUsers.clear();
|
visibleUsers.clear();
|
||||||
invisibleUsers.clear();
|
invisibleUsers.clear();
|
||||||
visibility.value = 0;
|
visibility.value = 0;
|
||||||
@ -215,7 +206,8 @@ class PostEditorController extends GetxController {
|
|||||||
'title': title,
|
'title': title,
|
||||||
'description': description,
|
'description': description,
|
||||||
'content': contentController.text,
|
'content': contentController.text,
|
||||||
'tags': tags,
|
'tags': tagController.getTags?.map((x) => {'alias': x}).toList() ??
|
||||||
|
List.empty(),
|
||||||
'attachments': attachments,
|
'attachments': attachments,
|
||||||
'visible_users': visibleUsers,
|
'visible_users': visibleUsers,
|
||||||
'invisible_users': invisibleUsers,
|
'invisible_users': invisibleUsers,
|
||||||
@ -267,7 +259,7 @@ class PostEditorController extends GetxController {
|
|||||||
descriptionController.text.isNotEmpty,
|
descriptionController.text.isNotEmpty,
|
||||||
contentController.text.isNotEmpty,
|
contentController.text.isNotEmpty,
|
||||||
attachments.isNotEmpty,
|
attachments.isNotEmpty,
|
||||||
tags.isNotEmpty
|
tagController.getTags?.isNotEmpty ?? false,
|
||||||
].any((x) => x);
|
].any((x) => x);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -275,9 +267,8 @@ class PostEditorController extends GetxController {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
_saveTimer?.cancel();
|
_saveTimer?.cancel();
|
||||||
|
|
||||||
titleController.dispose();
|
|
||||||
descriptionController.dispose();
|
|
||||||
contentController.dispose();
|
contentController.dispose();
|
||||||
|
tagController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -382,13 +382,6 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
|||||||
_editorController.editAttachment(context);
|
_editorController.editAttachment(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.tag),
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
onPressed: () {
|
|
||||||
_editorController.editCategoriesAndTags(context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
).paddingSymmetric(horizontal: 6, vertical: 8),
|
).paddingSymmetric(horizontal: 6, vertical: 8),
|
||||||
),
|
),
|
||||||
|
@ -33,7 +33,7 @@ abstract class SolianTheme {
|
|||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
colorScheme: ColorScheme.fromSeed(
|
colorScheme: ColorScheme.fromSeed(
|
||||||
brightness: brightness,
|
brightness: brightness,
|
||||||
seedColor: const Color.fromRGBO(154, 98, 91, 1),
|
seedColor: const Color.fromRGBO(103, 96, 193, 1),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -98,7 +98,6 @@ const i18nEnglish = {
|
|||||||
'unpinPost': 'Unpin this post',
|
'unpinPost': 'Unpin this post',
|
||||||
'postRestoreFromLocal': 'Restore from local',
|
'postRestoreFromLocal': 'Restore from local',
|
||||||
'postAutoSaveAt': 'Auto saved at @date',
|
'postAutoSaveAt': 'Auto saved at @date',
|
||||||
'postCategoriesAndTags': 'Categories n\' Tags',
|
|
||||||
'postVisibility': 'Visibility',
|
'postVisibility': 'Visibility',
|
||||||
'postVisibilityAll': 'Everyone',
|
'postVisibilityAll': 'Everyone',
|
||||||
'postVisibilityFriends': 'Friends',
|
'postVisibilityFriends': 'Friends',
|
||||||
@ -294,14 +293,12 @@ const i18nEnglish = {
|
|||||||
'accountStatusNeutral': 'Neutral',
|
'accountStatusNeutral': 'Neutral',
|
||||||
'accountStatusPositive': 'Positive',
|
'accountStatusPositive': 'Positive',
|
||||||
'bsCheckingServer': 'Checking Server Status',
|
'bsCheckingServer': 'Checking Server Status',
|
||||||
'bsCheckingServerFail':
|
'bsCheckingServerFail': 'Unable connect to server, check your network connection',
|
||||||
'Unable connect to server, check your network connection',
|
|
||||||
'bsCheckingServerDown': 'Server currently unavailable, please retry later',
|
'bsCheckingServerDown': 'Server currently unavailable, please retry later',
|
||||||
'bsAuthorizing': 'Authorizing',
|
'bsAuthorizing': 'Authorizing',
|
||||||
'bsEstablishingConn': 'Establishing Connection',
|
'bsEstablishingConn': 'Establishing Connection',
|
||||||
'bsPreparingData': 'Preparing User Data',
|
'bsPreparingData': 'Preparing User Data',
|
||||||
'bsRegisteringPushNotify': 'Enabling Push Notifications',
|
'bsRegisteringPushNotify': 'Enabling Push Notifications',
|
||||||
'postShareContent':
|
'postShareContent': '@content\n\n@username on the Solar Network\nCheck it out: @link',
|
||||||
'@content\n\n@username on the Solar Network\nCheck it out: @link',
|
|
||||||
'postShareSubject': '@username posted a post on the Solar Network',
|
'postShareSubject': '@username posted a post on the Solar Network',
|
||||||
};
|
};
|
||||||
|
@ -92,7 +92,6 @@ const i18nSimplifiedChinese = {
|
|||||||
'unpinPost': '取消置顶本帖',
|
'unpinPost': '取消置顶本帖',
|
||||||
'postRestoreFromLocal': '内容从本地暂存回复',
|
'postRestoreFromLocal': '内容从本地暂存回复',
|
||||||
'postAutoSaveAt': '已自动保存于 @date',
|
'postAutoSaveAt': '已自动保存于 @date',
|
||||||
'postCategoriesAndTags': '分类与标签',
|
|
||||||
'postVisibility': '帖子可见性',
|
'postVisibility': '帖子可见性',
|
||||||
'postVisibilityAll': '所有人可见',
|
'postVisibilityAll': '所有人可见',
|
||||||
'postVisibilityFriends': '仅好友可见',
|
'postVisibilityFriends': '仅好友可见',
|
||||||
|
96
lib/widgets/feed/feed_tags_field.dart
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:textfield_tags/textfield_tags.dart';
|
||||||
|
|
||||||
|
class TagsField extends StatelessWidget {
|
||||||
|
final List<String>? initialTags;
|
||||||
|
final String hintText;
|
||||||
|
|
||||||
|
const TagsField({
|
||||||
|
super.key,
|
||||||
|
this.initialTags,
|
||||||
|
required this.hintText,
|
||||||
|
required StringTagController<String> tagsController,
|
||||||
|
}) : _tagsController = tagsController;
|
||||||
|
|
||||||
|
final StringTagController<String> _tagsController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
height: 48,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
child: TextFieldTags<String>(
|
||||||
|
initialTags: initialTags,
|
||||||
|
letterCase: LetterCase.small,
|
||||||
|
textfieldTagsController: _tagsController,
|
||||||
|
textSeparators: const [' ', ','],
|
||||||
|
inputFieldBuilder: (context, inputFieldValues) {
|
||||||
|
return TextField(
|
||||||
|
controller: inputFieldValues.textEditingController,
|
||||||
|
focusNode: inputFieldValues.focusNode,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
hintText: hintText,
|
||||||
|
border: InputBorder.none,
|
||||||
|
prefixIconConstraints: BoxConstraints(
|
||||||
|
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
||||||
|
),
|
||||||
|
prefixIcon: inputFieldValues.tags.isNotEmpty
|
||||||
|
? SingleChildScrollView(
|
||||||
|
controller: inputFieldValues.tagScrollController,
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(
|
||||||
|
children: inputFieldValues.tags.map((String tag) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(20.0),
|
||||||
|
),
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
margin: const EdgeInsets.only(right: 10.0),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10.0, vertical: 4.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
InkWell(
|
||||||
|
child: Text(
|
||||||
|
'#$tag',
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
//print("$tag selected");
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4.0),
|
||||||
|
InkWell(
|
||||||
|
child: const Icon(
|
||||||
|
Icons.cancel,
|
||||||
|
size: 14.0,
|
||||||
|
color: Color.fromARGB(255, 233, 233, 233),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
inputFieldValues.onTagRemoved(tag);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
onChanged: inputFieldValues.onTagChanged,
|
||||||
|
onSubmitted: inputFieldValues.onTagSubmitted,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -19,6 +19,7 @@ class MarkdownTextContent extends StatelessWidget {
|
|||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
data: content,
|
data: content,
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
|
selectable: isSelectable,
|
||||||
styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context)).copyWith(
|
styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context)).copyWith(
|
||||||
horizontalRuleDecoration: BoxDecoration(
|
horizontalRuleDecoration: BoxDecoration(
|
||||||
border: Border(
|
border: Border(
|
||||||
|
@ -1,37 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:solian/controllers/post_editor_controller.dart';
|
|
||||||
import 'package:solian/widgets/posts/editor/post_tags_field.dart';
|
|
||||||
|
|
||||||
class PostEditorCategoriesDialog extends StatelessWidget {
|
|
||||||
final PostEditorController controller;
|
|
||||||
|
|
||||||
const PostEditorCategoriesDialog({super.key, required this.controller});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AlertDialog(
|
|
||||||
title: Text('postCategoriesAndTags'.tr),
|
|
||||||
content: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
TagsField(
|
|
||||||
initialTags:
|
|
||||||
controller.editTo.value?.tags?.map((x) => x.alias).toList(),
|
|
||||||
hintText: 'postTagsPlaceholder'.tr,
|
|
||||||
onUpdate: (value) {
|
|
||||||
controller.tags.value = value;
|
|
||||||
controller.tags.refresh();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
child: Text('confirm'.tr),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -54,8 +54,8 @@ class PostEditorVisibilityDialog extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
Obx(() {
|
Obx(() {
|
||||||
if (controller.visibility.value == 2 ||
|
if (controller.visibility.value != 2 &&
|
||||||
controller.visibility.value == 3) {
|
controller.visibility.value != 3) {
|
||||||
return const SizedBox(height: 8);
|
return const SizedBox(height: 8);
|
||||||
}
|
}
|
||||||
return const SizedBox();
|
return const SizedBox();
|
||||||
|
@ -1,204 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:solian/services.dart';
|
|
||||||
|
|
||||||
class TagsField extends StatefulWidget {
|
|
||||||
final List<String>? initialTags;
|
|
||||||
final String hintText;
|
|
||||||
final Function(List<String>) onUpdate;
|
|
||||||
|
|
||||||
const TagsField({
|
|
||||||
super.key,
|
|
||||||
this.initialTags,
|
|
||||||
required this.hintText,
|
|
||||||
required this.onUpdate,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<TagsField> createState() => _TagsFieldState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TagsFieldState extends State<TagsField> {
|
|
||||||
static final List<String> _dividers = [' ', ','];
|
|
||||||
|
|
||||||
late final _Debounceable<List<String>?, String> _debouncedSearch;
|
|
||||||
|
|
||||||
final List<String> _currentTags = List.empty(growable: true);
|
|
||||||
|
|
||||||
String? _currentSearchProbe;
|
|
||||||
List<String> _lastAutocompleteResult = List.empty();
|
|
||||||
TextEditingController? _textEditingController;
|
|
||||||
|
|
||||||
Future<List<String>?> _searchTags(String probe) async {
|
|
||||||
_currentSearchProbe = probe;
|
|
||||||
|
|
||||||
final client = ServiceFinder.configureClient('interactive');
|
|
||||||
final resp = await client.get(
|
|
||||||
'/tags?take=10&probe=$_currentSearchProbe',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (_currentSearchProbe != probe) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
_currentSearchProbe = null;
|
|
||||||
|
|
||||||
return resp.body.map((x) => x['alias']).toList().cast<String>();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_debouncedSearch = _debounce<List<String>?, String>(_searchTags);
|
|
||||||
if (widget.initialTags != null) {
|
|
||||||
_currentTags.addAll(widget.initialTags!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Autocomplete<String>(
|
|
||||||
optionsBuilder: (TextEditingValue textEditingValue) async {
|
|
||||||
final result = await _debouncedSearch(textEditingValue.text);
|
|
||||||
if (result == null) {
|
|
||||||
return _lastAutocompleteResult;
|
|
||||||
}
|
|
||||||
_lastAutocompleteResult = result;
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
onSelected: (String value) {
|
|
||||||
if (value.isEmpty) return;
|
|
||||||
if (!_currentTags.contains(value)) {
|
|
||||||
setState(() => _currentTags.add(value));
|
|
||||||
}
|
|
||||||
_textEditingController?.clear();
|
|
||||||
widget.onUpdate(_currentTags);
|
|
||||||
},
|
|
||||||
fieldViewBuilder: (context, controller, focusNode, onSubmitted) {
|
|
||||||
_textEditingController = controller;
|
|
||||||
return TextField(
|
|
||||||
controller: controller,
|
|
||||||
focusNode: focusNode,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
isDense: true,
|
|
||||||
hintText: widget.hintText,
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
prefixIconConstraints: BoxConstraints(
|
|
||||||
maxWidth: MediaQuery.of(context).size.width * 0.75,
|
|
||||||
),
|
|
||||||
prefixIcon: _currentTags.isNotEmpty
|
|
||||||
? SingleChildScrollView(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
child: Row(
|
|
||||||
children: _currentTags.map((String tag) {
|
|
||||||
return Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: const BorderRadius.all(
|
|
||||||
Radius.circular(20.0),
|
|
||||||
),
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
margin: const EdgeInsets.only(left: 7.5),
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 10.0, vertical: 4.0),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
InkWell(
|
|
||||||
child: Text(
|
|
||||||
'#$tag',
|
|
||||||
style: const TextStyle(color: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4.0),
|
|
||||||
InkWell(
|
|
||||||
child: const Icon(
|
|
||||||
Icons.cancel,
|
|
||||||
size: 14.0,
|
|
||||||
color: Color.fromARGB(255, 233, 233, 233),
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
setState(() => _currentTags.remove(tag));
|
|
||||||
widget.onUpdate(_currentTags);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
).paddingOnly(right: 5),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
onChanged: (value) {
|
|
||||||
for (final divider in _dividers) {
|
|
||||||
if (value.endsWith(divider)) {
|
|
||||||
final tagValue = value.substring(0, value.length - 1);
|
|
||||||
if (tagValue.isEmpty) return;
|
|
||||||
if (!_currentTags.contains(tagValue)) {
|
|
||||||
setState(() => _currentTags.add(tagValue));
|
|
||||||
}
|
|
||||||
controller.clear();
|
|
||||||
widget.onUpdate(_currentTags);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onSubmitted: (_) {
|
|
||||||
onSubmitted();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
typedef _Debounceable<S, T> = Future<S?> Function(T parameter);
|
|
||||||
|
|
||||||
_Debounceable<S, T> _debounce<S, T>(_Debounceable<S?, T> function) {
|
|
||||||
_DebounceTimer? debounceTimer;
|
|
||||||
|
|
||||||
return (T parameter) async {
|
|
||||||
if (debounceTimer != null && !debounceTimer!.isCompleted) {
|
|
||||||
debounceTimer!.cancel();
|
|
||||||
}
|
|
||||||
debounceTimer = _DebounceTimer();
|
|
||||||
try {
|
|
||||||
await debounceTimer!.future;
|
|
||||||
} catch (error) {
|
|
||||||
if (error is _CancelException) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
return function(parameter);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DebounceTimer {
|
|
||||||
_DebounceTimer() {
|
|
||||||
_timer = Timer(const Duration(milliseconds: 500), _onComplete);
|
|
||||||
}
|
|
||||||
|
|
||||||
late final Timer _timer;
|
|
||||||
final Completer<void> _completer = Completer<void>();
|
|
||||||
|
|
||||||
void _onComplete() {
|
|
||||||
_completer.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> get future => _completer.future;
|
|
||||||
|
|
||||||
bool get isCompleted => _completer.isCompleted;
|
|
||||||
|
|
||||||
void cancel() {
|
|
||||||
_timer.cancel();
|
|
||||||
_completer.completeError(const _CancelException());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CancelException implements Exception {
|
|
||||||
const _CancelException();
|
|
||||||
}
|
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 176 KiB |
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 582 B After Width: | Height: | Size: 589 B |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.3 KiB |
@ -337,14 +337,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.9"
|
version: "2.3.9"
|
||||||
easy_debounce:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: easy_debounce
|
|
||||||
sha256: f082609cfb8f37defb9e37fc28bc978c6712dedf08d4c5a26f820fa10165a236
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.3"
|
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -60,7 +60,6 @@ dependencies:
|
|||||||
flutter_cache_manager: ^3.3.3
|
flutter_cache_manager: ^3.3.3
|
||||||
flutter_markdown_selectionarea: ^0.6.17+1
|
flutter_markdown_selectionarea: ^0.6.17+1
|
||||||
shared_preferences: ^2.2.3
|
shared_preferences: ^2.2.3
|
||||||
easy_debounce: ^2.0.3
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@ -81,9 +80,7 @@ flutter:
|
|||||||
- assets/logo.png
|
- assets/logo.png
|
||||||
|
|
||||||
flutter_launcher_icons:
|
flutter_launcher_icons:
|
||||||
android:
|
android: "launcher_icon"
|
||||||
generate: "launcher_icon"
|
|
||||||
image_path: "assets/icon-fit.png"
|
|
||||||
ios: true
|
ios: true
|
||||||
image_path: "assets/icon.png"
|
image_path: "assets/icon.png"
|
||||||
min_sdk_android: 21
|
min_sdk_android: 21
|
||||||
|
BIN
web/favicon.png
Before Width: | Height: | Size: 582 B After Width: | Height: | Size: 589 B |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |