Merge PR #1518 into 2.0.0-beta.2

This commit is contained in:
Levi committed 2026-09-15 23:06:45 +08:00
commit d41e27234d
17 files changed
+624 -182

No files matched your search

@@ -15,6 +15,7 @@ import '../../../../core/utils/compact_count_formatter.dart';
import '../../../../core/window/app_window_chrome.dart';
import '../../../../shared/theme/app_design_tokens.dart';
import '../../../../shared/theme/app_palette.dart';
import '../../../../shared/widgets/app_swipe_tabs.dart';
import '../../../../shared/widgets/app_primary_button.dart';
import '../../../../shared/widgets/app_text_field.dart';
import '../../../../shared/widgets/app_tooltip.dart';
@@ -391,80 +392,86 @@ class _Content extends StatelessWidget {
child: _FilterBar(state: state, onFilter: onFilter),
),
Expanded(
child: CustomScrollView(
key: const ValueKey('extensions-list-scroll-view'),
controller: scrollController,
slivers: [
if ((state.loadingInstalled || state.loadingStore) && state.displayItems.isEmpty)
SliverPadding(
padding: EdgeInsets.fromLTRB(horizontalPadding, 0, horizontalPadding, 20),
sliver: const _ExtensionSkeletonGrid(key: ValueKey('extensions-initial-skeleton')),
)
else if (state.displayItems.isEmpty)
SliverFillRemaining(
child: Center(
child: Text(
context.l10n.noExtensions,
style: TextStyle(color: palette.textSecondary, fontSize: 14),
),
),
)
else
SliverPadding(
padding: EdgeInsets.fromLTRB(horizontalPadding, 0, horizontalPadding, 20),
sliver: SliverLayoutBuilder(
builder: (context, constraints) {
final width = constraints.crossAxisExtent;
final crossAxisCount = _extensionGridColumnCount(width);
return SliverGrid(
delegate: SliverChildBuilderDelegate(
(context, index) => _ExtensionCard(
item: state.displayItems[index],
busy: state.busyExtensionIds.contains(state.displayItems[index].id),
canUpdate: _canUpdate(state, state.displayItems[index]),
onAction: onItemAction,
onOpenDetails: onOpenDetails,
onOpenSettings: onOpenSettings,
),
childCount: state.displayItems.length,
),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
mainAxisSpacing: _extensionGridSpacing,
crossAxisSpacing: _extensionGridSpacing,
mainAxisExtent: 178,
),
);
},
),
),
if (state.listFilter == ExtensionListFilter.market && state.loadingMoreStore)
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(bottom: 24, top: 4),
child: Center(
child: const SizedBox.square(dimension: 18, child: shad.CircularProgressIndicator()),
),
),
),
if (state.listFilter == ExtensionListFilter.market &&
state.displayItems.isNotEmpty &&
state.storePagination != null &&
!state.storePagination!.hasNext &&
!state.loadingMoreStore)
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(bottom: 24, top: 4),
child: AppSwipeTabs<ExtensionListFilter>(
enabled: !isDesktop,
values: const [ExtensionListFilter.market, ExtensionListFilter.installed],
selectedValue: state.listFilter,
onSelected: onFilter,
child: CustomScrollView(
key: const ValueKey('extensions-list-scroll-view'),
controller: scrollController,
slivers: [
if ((state.loadingInstalled || state.loadingStore) && state.displayItems.isEmpty)
SliverPadding(
padding: EdgeInsets.fromLTRB(horizontalPadding, 0, horizontalPadding, 20),
sliver: const _ExtensionSkeletonGrid(key: ValueKey('extensions-initial-skeleton')),
)
else if (state.displayItems.isEmpty)
SliverFillRemaining(
child: Center(
child: Text(
context.l10n.extensionNoMore,
key: const ValueKey('extensions-no-more-indicator'),
style: TextStyle(color: palette.textMuted, fontSize: 12),
context.l10n.noExtensions,
style: TextStyle(color: palette.textSecondary, fontSize: 14),
),
),
)
else
SliverPadding(
padding: EdgeInsets.fromLTRB(horizontalPadding, 0, horizontalPadding, 20),
sliver: SliverLayoutBuilder(
builder: (context, constraints) {
final width = constraints.crossAxisExtent;
final crossAxisCount = _extensionGridColumnCount(width);
return SliverGrid(
delegate: SliverChildBuilderDelegate(
(context, index) => _ExtensionCard(
item: state.displayItems[index],
busy: state.busyExtensionIds.contains(state.displayItems[index].id),
canUpdate: _canUpdate(state, state.displayItems[index]),
onAction: onItemAction,
onOpenDetails: onOpenDetails,
onOpenSettings: onOpenSettings,
),
childCount: state.displayItems.length,
),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
mainAxisSpacing: _extensionGridSpacing,
crossAxisSpacing: _extensionGridSpacing,
mainAxisExtent: 178,
),
);
},
),
),
if (state.listFilter == ExtensionListFilter.market && state.loadingMoreStore)
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(bottom: 24, top: 4),
child: Center(
child: const SizedBox.square(dimension: 18, child: shad.CircularProgressIndicator()),
),
),
),
),
],
if (state.listFilter == ExtensionListFilter.market &&
state.displayItems.isNotEmpty &&
state.storePagination != null &&
!state.storePagination!.hasNext &&
!state.loadingMoreStore)
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(bottom: 24, top: 4),
child: Center(
child: Text(
context.l10n.extensionNoMore,
key: const ValueKey('extensions-no-more-indicator'),
style: TextStyle(color: palette.textMuted, fontSize: 12),
),
),
),
),
],
),
),
),
],
@@ -29,6 +29,8 @@ import '../../../../shared/theme/app_design_tokens.dart';
import '../../../../shared/theme/app_palette.dart';
import '../../../../shared/widgets/app_choice_segmented_control.dart';
import '../../../../shared/widgets/app_http_headers_editor.dart';
import '../../../../shared/widgets/app_form_row.dart';
import '../../../../shared/widgets/app_form_pair.dart';
import '../../../../shared/widgets/app_number_input.dart';
import '../../../../shared/widgets/app_path_picker_field.dart';
import '../../../../shared/widgets/app_text_field.dart';
@@ -274,7 +276,11 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
}
@override
Widget build(BuildContext context) {
Widget build(BuildContext context) => LayoutBuilder(
builder: (context, constraints) => _buildPanel(context, stacked: constraints.maxWidth < Breakpoints.mobile),
);
Widget _buildPanel(BuildContext context, {required bool stacked}) {
final palette = AppPalette.of(context);
final page = Scaffold(
@@ -307,7 +313,8 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
padding: const EdgeInsets.fromLTRB(24, 8, 24, 20),
child: Column(
children: [
_FormRow(
AppFormRow(
direction: stacked ? Axis.vertical : Axis.horizontal,
label: context.l10n.downloadLink,
child: DropTarget(
enable: !_creating,
@@ -364,7 +371,8 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
),
),
const SizedBox(height: 16),
_FormRow(
AppFormRow(
direction: stacked ? Axis.vertical : Axis.horizontal,
label: context.l10n.rename,
child: _WindowTextField(
key: const ValueKey('create-task-rename-input'),
@@ -373,7 +381,8 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
),
),
const SizedBox(height: 16),
_FormRow(
AppFormRow(
direction: stacked ? Axis.vertical : Axis.horizontal,
label: context.l10n.connections,
child: AppNumberInput(
fieldKey: const ValueKey('create-task-connections-input'),
@@ -384,7 +393,8 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
),
),
const SizedBox(height: 16),
_FormRow(
AppFormRow(
direction: stacked ? Axis.vertical : Axis.horizontal,
label: context.l10n.directory,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -407,6 +417,7 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
const SizedBox(height: 8),
_DirectoryOptions(
key: const ValueKey('create-task-directory-options-row'),
stacked: stacked,
asDefaultPath: _asDefaultPath,
onAsDefaultPathChanged: (value) => setState(() => _asDefaultPath = value),
shortcuts: [
@@ -428,7 +439,8 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
),
),
const SizedBox(height: 14),
_FormRow(
AppFormRow(
direction: stacked ? Axis.vertical : Axis.horizontal,
label: context.l10n.directDownload,
child: _DirectDownloadToggle(
value: _directDownload,
@@ -465,10 +477,12 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
padding: const EdgeInsets.only(top: 18, bottom: 4),
child: Column(
children: [
_FormRow(
AppFormRow(
direction: stacked ? Axis.vertical : Axis.horizontal,
label: context.l10n.proxy,
child: AppChoiceSegmentedControl<RequestProxyMode>(
key: const ValueKey('create-task-proxy-mode'),
showIcons: !stacked,
value: _proxyMode,
buttonKeyPrefix: 'create-task-proxy-mode',
alignment: WrapAlignment.start,
@@ -497,10 +511,12 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
),
if (_proxyMode == RequestProxyMode.custom) ...[
const SizedBox(height: 12),
_FormRow(
AppFormRow(
direction: stacked ? Axis.vertical : Axis.horizontal,
label: context.l10n.proxyProtocol,
child: AppChoiceSegmentedControl<String>(
key: const ValueKey('create-task-proxy-scheme'),
showIcons: !stacked,
value: _proxyScheme,
buttonKeyPrefix: 'create-task-proxy-scheme',
alignment: WrapAlignment.start,
@@ -513,59 +529,45 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
),
),
const SizedBox(height: 12),
_FormRow(
label: context.l10n.server,
child: Row(
children: [
Expanded(
flex: 618,
child: _WindowTextField(
key: const ValueKey('create-task-proxy-server'),
controller: _proxyServerController,
hintText: context.l10n.server,
),
),
const SizedBox(width: 8),
Expanded(
flex: 382,
child: _WindowTextField(
key: const ValueKey('create-task-proxy-port'),
controller: _proxyPortController,
hintText: context.l10n.port,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
),
),
],
AppFormPair(
direction: stacked ? Axis.vertical : Axis.horizontal,
firstLabel: context.l10n.server,
secondLabel: context.l10n.port,
firstFlex: 618,
secondFlex: 382,
first: _WindowTextField(
key: const ValueKey('create-task-proxy-server'),
controller: _proxyServerController,
hintText: context.l10n.server,
),
second: _WindowTextField(
key: const ValueKey('create-task-proxy-port'),
controller: _proxyPortController,
hintText: context.l10n.port,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
),
),
const SizedBox(height: 12),
_FormRow(
label: context.l10n.username,
child: Row(
children: [
Expanded(
child: _WindowTextField(
key: const ValueKey('create-task-proxy-username'),
controller: _proxyUsernameController,
hintText: context.l10n.username,
),
),
const SizedBox(width: 8),
Expanded(
child: _WindowTextField(
key: const ValueKey('create-task-proxy-password'),
controller: _proxyPasswordController,
hintText: context.l10n.password,
obscureText: true,
),
),
],
AppFormPair(
direction: stacked ? Axis.vertical : Axis.horizontal,
firstLabel: context.l10n.username,
secondLabel: context.l10n.password,
first: _WindowTextField(
key: const ValueKey('create-task-proxy-username'),
controller: _proxyUsernameController,
hintText: context.l10n.username,
),
second: _WindowTextField(
key: const ValueKey('create-task-proxy-password'),
controller: _proxyPasswordController,
hintText: context.l10n.password,
obscureText: true,
),
),
],
const SizedBox(height: 18),
Container(height: 1, color: palette.border),
if (!stacked) Container(height: 1, color: palette.border),
const SizedBox(height: 16),
Align(
alignment: Alignment.centerLeft,
@@ -597,11 +599,13 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
if (_protocolTab == 0) ...[
AppHttpHeadersEditor(
controller: _httpHeadersController,
stacked: stacked,
label: context.l10n.httpHeader,
keyPrefix: 'create-task-http-header',
),
const SizedBox(height: 14),
_FormRow(
AppFormRow(
direction: stacked ? Axis.vertical : Axis.horizontal,
label: context.l10n.skipVerifyCert,
child: _OptionSwitch(
key: const ValueKey('create-task-skip-verify-cert'),
@@ -610,7 +614,8 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
),
),
const SizedBox(height: 14),
_FormRow(
AppFormRow(
direction: stacked ? Axis.vertical : Axis.horizontal,
label: context.l10n.autoTorrentEnable,
child: _OptionSwitch(
key: const ValueKey('create-task-auto-torrent'),
@@ -620,7 +625,8 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
),
if (_autoTorrent == true) ...[
const SizedBox(height: 12),
_FormRow(
AppFormRow(
direction: stacked ? Axis.vertical : Axis.horizontal,
label: context.l10n.autoTorrentDeleteAfterDownload,
child: _OptionSwitch(
key: const ValueKey('create-task-delete-torrent'),
@@ -630,7 +636,8 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
),
],
const SizedBox(height: 14),
_FormRow(
AppFormRow(
direction: stacked ? Axis.vertical : Axis.horizontal,
label: context.l10n.autoExtract,
child: _OptionSwitch(
key: const ValueKey('create-task-auto-extract'),
@@ -640,7 +647,8 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
),
if (_autoExtract == true) ...[
const SizedBox(height: 12),
_FormRow(
AppFormRow(
direction: stacked ? Axis.vertical : Axis.horizontal,
label: context.l10n.archivePassword,
child: _WindowTextField(
key: const ValueKey('create-task-archive-password'),
@@ -650,7 +658,8 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
),
),
const SizedBox(height: 12),
_FormRow(
AppFormRow(
direction: stacked ? Axis.vertical : Axis.horizontal,
label: context.l10n.deleteAfterExtract,
child: _OptionSwitch(
key: const ValueKey('create-task-delete-after-extract'),
@@ -660,7 +669,8 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
),
],
] else ...[
_FormRow(
AppFormRow(
direction: stacked ? Axis.vertical : Axis.horizontal,
label: context.l10n.trackers,
child: SizedBox(
height: 96,
@@ -694,7 +704,7 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
child: Container(
padding: const EdgeInsets.fromLTRB(24, 12, 24, 20),
decoration: BoxDecoration(
border: Border(top: BorderSide(color: palette.border)),
border: stacked ? null : Border(top: BorderSide(color: palette.border)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
@@ -1380,35 +1390,6 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
enum _TaskProtocol { http, bt, ed2k }
class _FormRow extends StatelessWidget {
const _FormRow({required this.label, required this.child});
final String label;
final Widget child;
@override
Widget build(BuildContext context) {
final palette = AppPalette.of(context);
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 104,
child: Padding(
padding: const EdgeInsets.only(top: 10),
child: Text(
label,
style: TextStyle(color: palette.textPrimary, fontSize: 13, fontWeight: FontWeight.w500),
),
),
),
const SizedBox(width: 12),
Expanded(child: child),
],
);
}
}
class _DirectDownloadToggle extends StatelessWidget {
const _DirectDownloadToggle({required this.value, required this.onChanged});
@@ -1499,12 +1480,14 @@ class _DirectoryShortcut {
class _DirectoryOptions extends StatelessWidget {
const _DirectoryOptions({
super.key,
required this.stacked,
required this.asDefaultPath,
required this.onAsDefaultPathChanged,
required this.shortcuts,
required this.onShortcutSelected,
});
final bool stacked;
final bool asDefaultPath;
final ValueChanged<bool> onAsDefaultPathChanged;
final List<_DirectoryShortcut> shortcuts;
@@ -1512,7 +1495,6 @@ class _DirectoryOptions extends StatelessWidget {
@override
Widget build(BuildContext context) {
final stacked = shortcuts.isNotEmpty && MediaQuery.sizeOf(context).width < Breakpoints.mobile;
final toggle = _AsDefaultPathToggle(value: asDefaultPath, onChanged: onAsDefaultPathChanged);
final shortcutList = _DirectoryShortcuts(shortcuts: shortcuts, alignEnd: !stacked, onSelected: onShortcutSelected);
@@ -1557,6 +1539,21 @@ class _DirectoryShortcuts extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (!alignEnd) {
return Wrap(
key: const ValueKey('create-task-directory-shortcuts'),
spacing: AppDesignTokens.space8,
runSpacing: AppDesignTokens.space8,
children: [
for (final shortcut in shortcuts)
_DirectoryShortcutButton(
key: ValueKey('create-task-category-${shortcut.index}'),
shortcut: shortcut,
onPressed: () => onSelected(shortcut),
),
],
);
}
return SizedBox(
key: const ValueKey('create-task-directory-shortcuts'),
height: 28,
@@ -60,6 +60,7 @@ class _ResolveFileTreeState extends State<ResolveFileTree> {
Expanded(
child: FileTreeView<int>(
items: _treeItems,
initialExpandedDepth: 1,
keyPrefix: 'resolve-tree',
rowHeight: _resolveTreeRowHeight,
contentTextStyle: _resolveFileNameStyle,