mirror of
https://github.com/wgh136/pixes.git
synced 2025-09-27 12:57:24 +00:00
Compare commits
5 Commits
5dad6910fc
...
v1.2.0
Author | SHA1 | Date | |
---|---|---|---|
558832d188 | |||
2be88895ed | |||
1a110e711d | |||
1ad8329797 | |||
1cf4da66ad |
@@ -1,6 +1,6 @@
|
|||||||
# pixes
|
# pixes
|
||||||
|
|
||||||
[](https://flutter.dev/)
|
[](https://flutter.dev/)
|
||||||
[](https://github.com/wgh136/pixes/blob/master/LICENSE)
|
[](https://github.com/wgh136/pixes/blob/master/LICENSE)
|
||||||
[](https://github.com/wgh136/pixes)
|
[](https://github.com/wgh136/pixes)
|
||||||
[](https://github.com/wgh136/pixes/stargazers)
|
[](https://github.com/wgh136/pixes/stargazers)
|
||||||
|
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
|
@@ -19,7 +19,7 @@ pluginManagement {
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
id "com.android.application" version "8.2.1" apply false
|
id "com.android.application" version "8.9.0" apply false
|
||||||
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
|
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -184,7 +184,8 @@
|
|||||||
"Emphasize artworks from following artists": "强调关注画师的作品",
|
"Emphasize artworks from following artists": "强调关注画师的作品",
|
||||||
"The border of the artworks will be darker": "作品的边框将被加深",
|
"The border of the artworks will be darker": "作品的边框将被加深",
|
||||||
"Initial Page": "初始页面",
|
"Initial Page": "初始页面",
|
||||||
"Close the pane to apply the settings": "关闭面板以应用设置"
|
"Close the pane to apply the settings": "关闭面板以应用设置",
|
||||||
|
"No results found": "未找到结果"
|
||||||
},
|
},
|
||||||
"zh_TW": {
|
"zh_TW": {
|
||||||
"Search": "搜索",
|
"Search": "搜索",
|
||||||
@@ -371,6 +372,7 @@
|
|||||||
"Emphasize artworks from following artists": "強調關注畫師的作品",
|
"Emphasize artworks from following artists": "強調關注畫師的作品",
|
||||||
"The border of the artworks will be darker": "作品的邊框將被加深",
|
"The border of the artworks will be darker": "作品的邊框將被加深",
|
||||||
"Initial Page": "初始頁面",
|
"Initial Page": "初始頁面",
|
||||||
"Close the pane to apply the settings": "關閉面板以應用設置"
|
"Close the pane to apply the settings": "關閉面板以應用設置",
|
||||||
|
"No results found": "未找到結果"
|
||||||
}
|
}
|
||||||
}
|
}
|
315
lib/components/search_field.dart
Normal file
315
lib/components/search_field.dart
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:pixes/foundation/app.dart';
|
||||||
|
|
||||||
|
class AutoCompleteItem {
|
||||||
|
final String title;
|
||||||
|
final String? subtitle;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const AutoCompleteItem({
|
||||||
|
required this.title,
|
||||||
|
this.subtitle,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class AutoCompleteData {
|
||||||
|
final List<AutoCompleteItem> items;
|
||||||
|
final bool isLoading;
|
||||||
|
|
||||||
|
const AutoCompleteData({
|
||||||
|
this.items = const <AutoCompleteItem>[],
|
||||||
|
this.isLoading = false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class SearchField extends StatefulWidget {
|
||||||
|
const SearchField({
|
||||||
|
super.key,
|
||||||
|
this.autoCompleteItems = const <AutoCompleteItem>[],
|
||||||
|
this.isLoadingAutoCompleteItems = false,
|
||||||
|
this.enableAutoComplete = true,
|
||||||
|
this.textEditingController,
|
||||||
|
this.placeholder,
|
||||||
|
this.leading,
|
||||||
|
this.trailing,
|
||||||
|
this.foregroundDecoration,
|
||||||
|
this.onChanged,
|
||||||
|
this.onSubmitted,
|
||||||
|
this.padding,
|
||||||
|
this.focusNode,
|
||||||
|
this.autoCompleteNoResultsText,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<AutoCompleteItem> autoCompleteItems;
|
||||||
|
|
||||||
|
final bool isLoadingAutoCompleteItems;
|
||||||
|
|
||||||
|
final bool enableAutoComplete;
|
||||||
|
|
||||||
|
final TextEditingController? textEditingController;
|
||||||
|
|
||||||
|
final String? placeholder;
|
||||||
|
|
||||||
|
final Widget? leading;
|
||||||
|
|
||||||
|
final Widget? trailing;
|
||||||
|
|
||||||
|
final WidgetStatePropertyAll<BoxDecoration>? foregroundDecoration;
|
||||||
|
|
||||||
|
final void Function(String)? onChanged;
|
||||||
|
|
||||||
|
final void Function(String)? onSubmitted;
|
||||||
|
|
||||||
|
final EdgeInsets? padding;
|
||||||
|
|
||||||
|
final FocusNode? focusNode;
|
||||||
|
|
||||||
|
final String? autoCompleteNoResultsText;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SearchField> createState() => _SearchFieldState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SearchFieldState extends State<SearchField> with TickerProviderStateMixin {
|
||||||
|
late final ValueNotifier<AutoCompleteData> autoCompleteItems;
|
||||||
|
|
||||||
|
late final FocusNode focusNode;
|
||||||
|
|
||||||
|
final boxKey = GlobalKey();
|
||||||
|
|
||||||
|
OverlayEntry? _overlayEntry;
|
||||||
|
|
||||||
|
AnimationController? _animationController;
|
||||||
|
Animation<double>? _fadeAnimation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
autoCompleteItems = ValueNotifier(AutoCompleteData(
|
||||||
|
items: widget.autoCompleteItems,
|
||||||
|
isLoading: widget.isLoadingAutoCompleteItems,
|
||||||
|
));
|
||||||
|
focusNode = widget.focusNode ?? FocusNode();
|
||||||
|
focusNode.addListener(onfocusChange);
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_animationController?.dispose();
|
||||||
|
focusNode.removeListener(onfocusChange);
|
||||||
|
if (widget.focusNode == null) {
|
||||||
|
focusNode.dispose();
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant SearchField oldWidget) {
|
||||||
|
if (widget.autoCompleteItems != oldWidget.autoCompleteItems ||
|
||||||
|
widget.isLoadingAutoCompleteItems !=
|
||||||
|
oldWidget.isLoadingAutoCompleteItems) {
|
||||||
|
Future.microtask(() {
|
||||||
|
autoCompleteItems.value = AutoCompleteData(
|
||||||
|
items: widget.autoCompleteItems,
|
||||||
|
isLoading: widget.isLoadingAutoCompleteItems,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onfocusChange() {
|
||||||
|
if (focusNode.hasFocus && widget.enableAutoComplete) {
|
||||||
|
final box = context.findRenderObject() as RenderBox?;
|
||||||
|
if (box == null) return;
|
||||||
|
final overlay = Overlay.of(context);
|
||||||
|
final position = box.localToGlobal(
|
||||||
|
Offset.zero,
|
||||||
|
ancestor: overlay.context.findRenderObject(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (_overlayEntry != null) {
|
||||||
|
_removeOverlayWithAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
_animationController = AnimationController(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
|
||||||
|
_fadeAnimation = Tween<double>(
|
||||||
|
begin: 0.0,
|
||||||
|
end: 1.0,
|
||||||
|
).animate(CurvedAnimation(
|
||||||
|
parent: _animationController!,
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
));
|
||||||
|
|
||||||
|
_overlayEntry = OverlayEntry(
|
||||||
|
builder: (context) {
|
||||||
|
return Positioned(
|
||||||
|
left: position.dx,
|
||||||
|
width: box.size.width,
|
||||||
|
top: position.dy + box.size.height,
|
||||||
|
child: _AnimatedOverlayWrapper(
|
||||||
|
animation: _fadeAnimation!,
|
||||||
|
child: _AutoCompleteOverlay(
|
||||||
|
data: autoCompleteItems,
|
||||||
|
noResultsText: widget.autoCompleteNoResultsText,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
overlay.insert(_overlayEntry!);
|
||||||
|
_animationController!.forward();
|
||||||
|
} else {
|
||||||
|
_removeOverlayWithAnimation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _removeOverlayWithAnimation() {
|
||||||
|
if (_overlayEntry != null && _animationController != null) {
|
||||||
|
_animationController!.reverse().then((_) {
|
||||||
|
_overlayEntry?.remove();
|
||||||
|
_overlayEntry = null;
|
||||||
|
_animationController?.dispose();
|
||||||
|
_animationController = null;
|
||||||
|
_fadeAnimation = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return TextBox(
|
||||||
|
controller: widget.textEditingController,
|
||||||
|
key: boxKey,
|
||||||
|
focusNode: focusNode,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
placeholder: widget.placeholder,
|
||||||
|
onChanged: widget.onChanged,
|
||||||
|
onSubmitted: widget.onSubmitted,
|
||||||
|
foregroundDecoration: widget.foregroundDecoration,
|
||||||
|
prefix: widget.leading,
|
||||||
|
suffix: widget.trailing,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AutoCompleteOverlay extends StatefulWidget {
|
||||||
|
const _AutoCompleteOverlay({required this.data, this.noResultsText});
|
||||||
|
|
||||||
|
final ValueNotifier<AutoCompleteData> data;
|
||||||
|
|
||||||
|
final String? noResultsText;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_AutoCompleteOverlay> createState() => _AutoCompleteOverlayState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AutoCompleteOverlayState extends State<_AutoCompleteOverlay> {
|
||||||
|
late final notifier = widget.data;
|
||||||
|
|
||||||
|
var items = <AutoCompleteItem>[];
|
||||||
|
|
||||||
|
var isLoading = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
items = notifier.value.items;
|
||||||
|
isLoading = notifier.value.isLoading;
|
||||||
|
notifier.addListener(onItemsChanged);
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
notifier.removeListener(onItemsChanged);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void onItemsChanged() {
|
||||||
|
setState(() {
|
||||||
|
items = notifier.value.items;
|
||||||
|
isLoading = notifier.value.isLoading;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var items = List<AutoCompleteItem>.from(this.items);
|
||||||
|
|
||||||
|
Widget? content;
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
content = SizedBox(
|
||||||
|
height: 44,
|
||||||
|
child: Center(
|
||||||
|
child: ProgressRing(
|
||||||
|
activeColor: FluentTheme.of(context).accentColor,
|
||||||
|
strokeWidth: 2,
|
||||||
|
).fixWidth(24).fixHeight(24),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (items.isEmpty) {
|
||||||
|
content = ListTile(
|
||||||
|
title: Text(widget.noResultsText ?? 'No results found'),
|
||||||
|
onPressed: () {},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (items.length > 8) {
|
||||||
|
items = items.sublist(0, 8);
|
||||||
|
}
|
||||||
|
content = Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: items.map((item) {
|
||||||
|
return ListTile(
|
||||||
|
title: Text(item.title),
|
||||||
|
subtitle: item.subtitle != null ? Text(item.subtitle!) : null,
|
||||||
|
onPressed: item.onTap,
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
backgroundColor: FluentTheme.of(context).micaBackgroundColor,
|
||||||
|
child: AnimatedSize(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
duration: const Duration(milliseconds: 160),
|
||||||
|
child: content,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AnimatedOverlayWrapper extends StatelessWidget {
|
||||||
|
const _AnimatedOverlayWrapper({
|
||||||
|
required this.animation,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Animation<double> animation;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: animation,
|
||||||
|
builder: (context, child) {
|
||||||
|
return FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: Transform.scale(
|
||||||
|
scale: 0.9 + (0.1 * animation.value),
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -12,7 +12,7 @@ export "state_controller.dart";
|
|||||||
export "navigation.dart";
|
export "navigation.dart";
|
||||||
|
|
||||||
class _App {
|
class _App {
|
||||||
final version = "1.1.1";
|
final version = "1.2.0";
|
||||||
|
|
||||||
bool get isAndroid => Platform.isAndroid;
|
bool get isAndroid => Platform.isAndroid;
|
||||||
bool get isIOS => Platform.isIOS;
|
bool get isIOS => Platform.isIOS;
|
||||||
|
@@ -152,6 +152,10 @@ class Tag {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => name.hashCode;
|
int get hashCode => name.hashCode;
|
||||||
|
|
||||||
|
static Tag fromJson(Map<String, dynamic> json) {
|
||||||
|
return Tag(json['name'] ?? "", json['translated_name']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class IllustImage {
|
class IllustImage {
|
||||||
|
@@ -583,4 +583,13 @@ class Network {
|
|||||||
return Res.fromErrorRes(res);
|
return Res.fromErrorRes(res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Res<List<Tag>>> getAutoCompleteTags(String keyword) async {
|
||||||
|
var res = await apiGet("/v2/search/autocomplete?merge_plain_keyword_results=true&word=${Uri.encodeComponent(keyword)}");
|
||||||
|
if (res.success) {
|
||||||
|
return Res((res.data["tags"] as List).map((e) => Tag.fromJson(e)).toList());
|
||||||
|
} else {
|
||||||
|
return Res.error(res.errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,7 @@ import 'dart:io';
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:image_gallery_saver/image_gallery_saver.dart';
|
import 'package:image_gallery_saver_plus/image_gallery_saver_plus.dart';
|
||||||
import 'package:photo_view/photo_view_gallery.dart';
|
import 'package:photo_view/photo_view_gallery.dart';
|
||||||
import 'package:pixes/components/md.dart';
|
import 'package:pixes/components/md.dart';
|
||||||
import 'package:pixes/components/message.dart';
|
import 'package:pixes/components/message.dart';
|
||||||
@@ -118,10 +118,11 @@ class _ImagePageState extends State<ImagePage> with WindowListener {
|
|||||||
if (!fileName.contains('.')) {
|
if (!fileName.contains('.')) {
|
||||||
fileName += getExtensionName();
|
fileName += getExtensionName();
|
||||||
}
|
}
|
||||||
await ImageGallerySaver.saveImage(
|
await ImageGallerySaverPlus.saveImage(
|
||||||
await file.readAsBytes(),
|
await file.readAsBytes(),
|
||||||
quality: 100,
|
quality: 100,
|
||||||
name: fileName);
|
name: fileName,
|
||||||
|
);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
showToast(context, message: "Saved".tl);
|
showToast(context, message: "Saved".tl);
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,7 @@ import 'package:pixes/appdata.dart';
|
|||||||
import 'package:pixes/components/loading.dart';
|
import 'package:pixes/components/loading.dart';
|
||||||
import 'package:pixes/components/novel.dart';
|
import 'package:pixes/components/novel.dart';
|
||||||
import 'package:pixes/components/page_route.dart';
|
import 'package:pixes/components/page_route.dart';
|
||||||
|
import 'package:pixes/components/search_field.dart';
|
||||||
import 'package:pixes/components/user_preview.dart';
|
import 'package:pixes/components/user_preview.dart';
|
||||||
import 'package:pixes/foundation/app.dart';
|
import 'package:pixes/foundation/app.dart';
|
||||||
import 'package:pixes/network/network.dart';
|
import 'package:pixes/network/network.dart';
|
||||||
@@ -12,6 +13,7 @@ import 'package:pixes/pages/novel_page.dart';
|
|||||||
import 'package:pixes/pages/user_info_page.dart';
|
import 'package:pixes/pages/user_info_page.dart';
|
||||||
import 'package:pixes/utils/app_links.dart';
|
import 'package:pixes/utils/app_links.dart';
|
||||||
import 'package:pixes/utils/block.dart';
|
import 'package:pixes/utils/block.dart';
|
||||||
|
import 'package:pixes/utils/debounce.dart';
|
||||||
import 'package:pixes/utils/ext.dart';
|
import 'package:pixes/utils/ext.dart';
|
||||||
import 'package:pixes/utils/translation.dart';
|
import 'package:pixes/utils/translation.dart';
|
||||||
|
|
||||||
@@ -21,19 +23,7 @@ import '../components/illust_widget.dart';
|
|||||||
import '../components/md.dart';
|
import '../components/md.dart';
|
||||||
import '../foundation/image_provider.dart';
|
import '../foundation/image_provider.dart';
|
||||||
|
|
||||||
class SearchPage extends StatefulWidget {
|
const searchTypes = [
|
||||||
const SearchPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<SearchPage> createState() => _SearchPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SearchPageState extends State<SearchPage> {
|
|
||||||
String text = "";
|
|
||||||
|
|
||||||
int searchType = 0;
|
|
||||||
|
|
||||||
static const searchTypes = [
|
|
||||||
"Search artwork",
|
"Search artwork",
|
||||||
"Search novel",
|
"Search novel",
|
||||||
"Search user",
|
"Search user",
|
||||||
@@ -42,7 +32,17 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
"Novel ID"
|
"Novel ID"
|
||||||
];
|
];
|
||||||
|
|
||||||
void search() {
|
class SearchPage extends StatefulWidget {
|
||||||
|
const SearchPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SearchPage> createState() => _SearchPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SearchPageState extends State<SearchPage> {
|
||||||
|
int searchType = 0;
|
||||||
|
|
||||||
|
void search(String text) {
|
||||||
if (text.isURL && handleLink(Uri.parse(text))) {
|
if (text.isURL && handleLink(Uri.parse(text))) {
|
||||||
return;
|
return;
|
||||||
} else if ("https://$text".isURL &&
|
} else if ("https://$text".isURL &&
|
||||||
@@ -71,9 +71,19 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
padding: const EdgeInsets.only(top: 8),
|
padding: const EdgeInsets.only(top: 8),
|
||||||
content: Column(
|
content: Column(
|
||||||
children: [
|
children: [
|
||||||
buildSearchBar(),
|
_SearchBar(
|
||||||
const SizedBox(
|
searchType: searchType,
|
||||||
height: 8,
|
onTypeChanged: (type) {
|
||||||
|
setState(() {
|
||||||
|
searchType = type;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSearch: (text) {
|
||||||
|
if (text.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
search(text);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const Expanded(
|
const Expanded(
|
||||||
child: _TrendingTagsView(),
|
child: _TrendingTagsView(),
|
||||||
@@ -82,102 +92,6 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final optionController = FlyoutController();
|
|
||||||
|
|
||||||
Widget buildSearchBar() {
|
|
||||||
return ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints(maxWidth: 560),
|
|
||||||
child: SizedBox(
|
|
||||||
height: 42,
|
|
||||||
width: double.infinity,
|
|
||||||
child: LayoutBuilder(
|
|
||||||
builder: (context, constrains) {
|
|
||||||
return SizedBox(
|
|
||||||
height: 42,
|
|
||||||
width: constrains.maxWidth,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: TextBox(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
|
||||||
placeholder:
|
|
||||||
'${searchTypes[searchType].tl} / ${"Open link".tl}',
|
|
||||||
onChanged: (s) => text = s,
|
|
||||||
onSubmitted: (s) => search(),
|
|
||||||
foregroundDecoration: WidgetStatePropertyAll(
|
|
||||||
BoxDecoration(
|
|
||||||
border: Border.all(
|
|
||||||
color: ColorScheme.of(context)
|
|
||||||
.outlineVariant
|
|
||||||
.toOpacity(0.6)),
|
|
||||||
borderRadius: BorderRadius.circular(4))),
|
|
||||||
suffix: MouseRegion(
|
|
||||||
cursor: SystemMouseCursors.click,
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: search,
|
|
||||||
child: const Icon(
|
|
||||||
FluentIcons.search,
|
|
||||||
size: 16,
|
|
||||||
).paddingHorizontal(12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: 4,
|
|
||||||
),
|
|
||||||
FlyoutTarget(
|
|
||||||
controller: optionController,
|
|
||||||
child: Button(
|
|
||||||
child: const SizedBox(
|
|
||||||
height: 42,
|
|
||||||
child: Center(
|
|
||||||
child: Icon(FluentIcons.chevron_down),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
optionController.showFlyout(
|
|
||||||
placementMode: FlyoutPlacementMode.bottomCenter,
|
|
||||||
builder: buildSearchOption,
|
|
||||||
barrierColor: Colors.transparent);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: 4,
|
|
||||||
),
|
|
||||||
Button(
|
|
||||||
child: const SizedBox(
|
|
||||||
height: 42,
|
|
||||||
child: Center(
|
|
||||||
child: Icon(FluentIcons.settings),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).push(SideBarRoute(SearchSettings(
|
|
||||||
isNovel: searchType == 1,
|
|
||||||
)));
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
).paddingHorizontal(16),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildSearchOption(BuildContext context) {
|
|
||||||
return MenuFlyout(
|
|
||||||
items: List.generate(
|
|
||||||
searchTypes.length,
|
|
||||||
(index) => MenuFlyoutItem(
|
|
||||||
text: Text(searchTypes[index].tl),
|
|
||||||
onPressed: () => setState(() => searchType = index))),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TrendingTagsView extends StatefulWidget {
|
class _TrendingTagsView extends StatefulWidget {
|
||||||
@@ -803,3 +717,184 @@ class _SearchNovelResultPageState
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _SearchBar extends StatefulWidget {
|
||||||
|
const _SearchBar({
|
||||||
|
required this.searchType,
|
||||||
|
required this.onTypeChanged,
|
||||||
|
required this.onSearch,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int searchType;
|
||||||
|
|
||||||
|
final void Function(int) onTypeChanged;
|
||||||
|
|
||||||
|
final void Function(String) onSearch;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_SearchBar> createState() => _SearchBarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SearchBarState extends State<_SearchBar> {
|
||||||
|
final optionController = FlyoutController();
|
||||||
|
|
||||||
|
final textController = TextEditingController();
|
||||||
|
|
||||||
|
var autoCompleteItems = <AutoCompleteItem>[];
|
||||||
|
|
||||||
|
var debouncer = Debounce(delay: const Duration(milliseconds: 300));
|
||||||
|
|
||||||
|
var autoCompleteKey = 0;
|
||||||
|
|
||||||
|
var isLoadingAutoCompleteItems = false;
|
||||||
|
|
||||||
|
Widget buildSearchOption(BuildContext context) {
|
||||||
|
return MenuFlyout(
|
||||||
|
items: List.generate(
|
||||||
|
searchTypes.length,
|
||||||
|
(index) => MenuFlyoutItem(
|
||||||
|
text: Text(searchTypes[index].tl),
|
||||||
|
onPressed: () => widget.onTypeChanged(index),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onTextChanged(String text) {
|
||||||
|
if (widget.searchType == 3 ||
|
||||||
|
widget.searchType == 4 ||
|
||||||
|
widget.searchType == 5) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.isEmpty) {
|
||||||
|
setState(() {
|
||||||
|
autoCompleteItems = [];
|
||||||
|
isLoadingAutoCompleteItems = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
isLoadingAutoCompleteItems = true;
|
||||||
|
});
|
||||||
|
debouncer.call(() async {
|
||||||
|
var key = ++autoCompleteKey;
|
||||||
|
|
||||||
|
var res = await Network().getAutoCompleteTags(text);
|
||||||
|
if (res.error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var items = res.data.map((e) {
|
||||||
|
return AutoCompleteItem(
|
||||||
|
title: e.name,
|
||||||
|
subtitle: e.translatedName,
|
||||||
|
onTap: () {
|
||||||
|
textController.text = e.name;
|
||||||
|
widget.onSearch(e.name);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
if (key != autoCompleteKey) {
|
||||||
|
return; // ignore old request
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
autoCompleteItems = items;
|
||||||
|
isLoadingAutoCompleteItems = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 560),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 42,
|
||||||
|
width: double.infinity,
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constrains) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 42,
|
||||||
|
width: constrains.maxWidth,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: SearchField(
|
||||||
|
enableAutoComplete: widget.searchType != 3 &&
|
||||||
|
widget.searchType != 4 &&
|
||||||
|
widget.searchType != 5,
|
||||||
|
textEditingController: textController,
|
||||||
|
autoCompleteNoResultsText: "No results found".tl,
|
||||||
|
isLoadingAutoCompleteItems: isLoadingAutoCompleteItems,
|
||||||
|
autoCompleteItems: autoCompleteItems,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
placeholder:
|
||||||
|
'${searchTypes[widget.searchType].tl} / ${"Open link".tl}',
|
||||||
|
onChanged: onTextChanged,
|
||||||
|
onSubmitted: widget.onSearch,
|
||||||
|
foregroundDecoration: WidgetStatePropertyAll(
|
||||||
|
BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: ColorScheme.of(context)
|
||||||
|
.outlineVariant
|
||||||
|
.toOpacity(0.6),
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: MouseRegion(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => widget.onSearch(textController.text),
|
||||||
|
child: const Icon(
|
||||||
|
FluentIcons.search,
|
||||||
|
size: 16,
|
||||||
|
).paddingHorizontal(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
FlyoutTarget(
|
||||||
|
controller: optionController,
|
||||||
|
child: Button(
|
||||||
|
child: const SizedBox(
|
||||||
|
height: 42,
|
||||||
|
child: Center(
|
||||||
|
child: Icon(FluentIcons.chevron_down),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
optionController.showFlyout(
|
||||||
|
placementMode: FlyoutPlacementMode.bottomCenter,
|
||||||
|
builder: buildSearchOption,
|
||||||
|
barrierColor: Colors.transparent,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Button(
|
||||||
|
child: const SizedBox(
|
||||||
|
height: 42,
|
||||||
|
child: Center(
|
||||||
|
child: Icon(FluentIcons.settings),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).push(SideBarRoute(SearchSettings(
|
||||||
|
isNovel: widget.searchType == 1,
|
||||||
|
)));
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).paddingHorizontal(16),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
28
lib/utils/debounce.dart
Normal file
28
lib/utils/debounce.dart
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
class Debounce {
|
||||||
|
final Duration delay;
|
||||||
|
VoidCallback? _action;
|
||||||
|
Timer? _timer;
|
||||||
|
|
||||||
|
Debounce({required this.delay});
|
||||||
|
|
||||||
|
void call(VoidCallback action) {
|
||||||
|
_action = action;
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = Timer(delay, _execute);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _execute() {
|
||||||
|
if (_action != null) {
|
||||||
|
_action!();
|
||||||
|
_action = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void cancel() {
|
||||||
|
_timer?.cancel();
|
||||||
|
_action = null;
|
||||||
|
}
|
||||||
|
}
|
13
pubspec.lock
13
pubspec.lock
@@ -325,15 +325,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.2"
|
version: "4.1.2"
|
||||||
image_gallery_saver:
|
image_gallery_saver_plus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
name: image_gallery_saver_plus
|
||||||
ref: master
|
sha256: "199b9e24f8d85e98f11e3d35571ab68ae50626ad40e2bb85c84383f69a6950ad"
|
||||||
resolved-ref: "38a38c45d3ed229cbc1d827eb2b5aaad1a4519cd"
|
url: "https://pub.dev"
|
||||||
url: "https://github.com/wgh136/image_gallery_saver"
|
source: hosted
|
||||||
source: git
|
version: "4.0.1"
|
||||||
version: "2.0.0"
|
|
||||||
intl:
|
intl:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 1.1.1+111
|
version: 1.2.0+120
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.3.4 <4.0.0'
|
sdk: '>=3.3.4 <4.0.0'
|
||||||
@@ -60,10 +60,7 @@ dependencies:
|
|||||||
webview_flutter: ^4.13.0
|
webview_flutter: ^4.13.0
|
||||||
flutter_acrylic: 1.0.0+2
|
flutter_acrylic: 1.0.0+2
|
||||||
device_info_plus: ^11.5.0
|
device_info_plus: ^11.5.0
|
||||||
image_gallery_saver:
|
image_gallery_saver_plus: ^4.0.1
|
||||||
git:
|
|
||||||
url: https://github.com/wgh136/image_gallery_saver
|
|
||||||
ref: master
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
Reference in New Issue
Block a user