refactor: remove proxy API (#1596)

This commit is contained in:
Levi authored and GitHub committed 2026-10-21 22:48:17 +08:00
1 parent 47cafb7575
commit 4214765d39
98 files changed
+5958 -1225

No files matched your search

@@ -8,6 +8,7 @@ import '../../../../shared/theme/app_palette.dart';
import '../../../../shared/widgets/app_loading_button.dart';
import '../../../../util/package_info.dart';
import '../../../../util/updater.dart';
import '../../../../core/network/candidate_download.dart';
import '../../../../util/util.dart';
import '../../../../l10n/l10n.dart';
import 'app_update_notes_viewport.dart';
@@ -57,7 +58,9 @@ Future<void> showAppUpdateDialog(
if (!dialogContext.mounted) return;
setDialogState(() {
updating = false;
errorMessage = dialogContext.l10n.updateFailed;
errorMessage = error is DownloadChecksumException
? dialogContext.l10n.updateVerificationFailed
: dialogContext.l10n.updateFailed;
});
}
}
@@ -137,7 +140,13 @@ Future<void> showAppUpdateDialog(
children: [
Icon(Icons.error_outline, size: 15, color: palette.error),
const SizedBox(width: 6),
Text(errorMessage!, style: TextStyle(color: palette.error, fontSize: 12)),
Flexible(
child: Text(
errorMessage!,
textAlign: TextAlign.center,
style: TextStyle(color: palette.error, fontSize: 12),
),
),
],
),
],
@@ -12,18 +12,20 @@ class SettingsItem extends StatelessWidget {
this.subtitle,
this.subtitleAction,
this.titleTrailing,
this.stacked = false,
});
final String title;
final String? subtitle;
final Widget? subtitleAction;
final Widget? titleTrailing;
final bool stacked;
final Widget child;
@override
Widget build(BuildContext context) {
final desktop = MediaQuery.sizeOf(context).width >= Breakpoints.mobile;
if (desktop) {
if (desktop && !stacked) {
return LayoutBuilder(
builder: (context, constraints) {
const leadingSpace = 2.0;
@@ -105,7 +105,7 @@ class SettingsListEditor extends StatelessWidget {
children: [
const Icon(Icons.add, size: 17),
const SizedBox(width: AppDesignTokens.space8),
Text(addLabel ?? context.l10n.add),
Flexible(child: Text(addLabel ?? context.l10n.add)),
],
),
),
@@ -0,0 +1,106 @@
import 'package:shadcn_flutter/shadcn_flutter.dart';
import '../../../../api/model/downloader_config.dart';
import '../../../../l10n/l10n.dart';
import '../../../../shared/theme/app_design_tokens.dart';
import '../../../../shared/widgets/app_loading_button.dart';
import 'settings_list_editor.dart';
import 'tracker_subscriptions_schedule.dart';
import 'tracker_subscriptions_table.dart';
class TrackerSubscriptionsControl extends StatefulWidget {
const TrackerSubscriptionsControl({
super.key,
required this.urls,
required this.statuses,
required this.autoUpdate,
required this.lastUpdated,
required this.onChanged,
required this.onAutoUpdateChanged,
required this.onUpdate,
});
final List<String> urls;
final Map<String, TrackerSubscriptionStatus> statuses;
final bool autoUpdate;
final DateTime? lastUpdated;
final ValueChanged<List<String>> onChanged;
final ValueChanged<bool> onAutoUpdateChanged;
final Future<void> Function() onUpdate;
@override
State<TrackerSubscriptionsControl> createState() => _TrackerSubscriptionsControlState();
}
class _TrackerSubscriptionsControlState extends State<TrackerSubscriptionsControl> {
bool _updating = false;
@override
Widget build(BuildContext context) {
return SizedBox(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TrackerSubscriptionsSchedule(
lastUpdated: widget.lastUpdated,
autoUpdate: widget.autoUpdate,
onAutoUpdateChanged: widget.onAutoUpdateChanged,
),
const SizedBox(height: AppDesignTokens.space12),
TrackerSubscriptionsTable(urls: widget.urls, statuses: widget.statuses, onDelete: _delete),
const SizedBox(height: AppDesignTokens.space12),
Wrap(
key: const ValueKey('tracker-subscriptions-actions'),
alignment: WrapAlignment.end,
crossAxisAlignment: WrapCrossAlignment.center,
spacing: AppDesignTokens.space8,
runSpacing: AppDesignTokens.space8,
children: [
AppLoadingButton(
key: const ValueKey('tracker-update-button'),
onPressed: _update,
loading: _updating,
icon: const Icon(Icons.refresh, size: 17),
child: Text(context.l10n.newVersionUpdate),
),
SecondaryButton(
key: const ValueKey('add-tracker-subscription'),
onPressed: _add,
leading: const Icon(Icons.add, size: 17),
child: Text(context.l10n.add),
),
],
),
],
),
);
}
Future<void> _add() async {
final value = await showTextSettingDialog(
context,
title: context.l10n.addTrackerSubscription,
fieldLabel: context.l10n.trackerSubscriptionUrl,
requireHttpUrl: true,
);
if (!mounted || value == null) return;
final next = [...widget.urls, value];
widget.onChanged(next.toSet().toList());
}
Future<void> _delete(int index) async {
final url = widget.urls[index];
if (!await showDeleteSettingsEntryDialog(context, url) || !mounted) return;
widget.onChanged(List<String>.from(widget.urls)..remove(url));
}
Future<void> _update() async {
setState(() => _updating = true);
try {
await widget.onUpdate();
} finally {
if (mounted) setState(() => _updating = false);
}
}
}
@@ -0,0 +1,56 @@
import 'package:shadcn_flutter/shadcn_flutter.dart';
import '../../../../l10n/l10n.dart';
import '../../../../shared/theme/app_design_tokens.dart';
import '../../../../shared/theme/app_palette.dart';
class TrackerSubscriptionsSchedule extends StatelessWidget {
const TrackerSubscriptionsSchedule({
super.key,
required this.lastUpdated,
required this.autoUpdate,
required this.onAutoUpdateChanged,
});
final DateTime? lastUpdated;
final bool autoUpdate;
final ValueChanged<bool> onAutoUpdateChanged;
@override
Widget build(BuildContext context) {
final palette = AppPalette.of(context);
return Column(
key: const ValueKey('tracker-subscriptions-schedule'),
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Row(
key: const ValueKey('tracker-auto-update-control'),
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(context.l10n.updateEveryDay, style: TextStyle(color: palette.textSecondary, fontSize: 12)),
),
const SizedBox(width: AppDesignTokens.space8),
Switch(value: autoUpdate, onChanged: onAutoUpdateChanged),
],
),
if (lastUpdated case final time?) ...[
const SizedBox(height: AppDesignTokens.space4),
Text(
key: const ValueKey('tracker-last-updated'),
context.l10n.lastUpdated(_formatDateTime(time)),
textAlign: TextAlign.end,
style: TextStyle(color: palette.textMuted, fontSize: 11),
),
],
],
);
}
String _formatDateTime(DateTime time) {
final value = time.toLocal();
String two(int number) => number.toString().padLeft(2, '0');
return '${value.year}-${two(value.month)}-${two(value.day)} ${two(value.hour)}:${two(value.minute)}:${two(value.second)}';
}
}
@@ -0,0 +1,258 @@
import 'dart:math' as math;
import 'package:shadcn_flutter/shadcn_flutter.dart';
import '../../../../api/model/downloader_config.dart';
import '../../../../l10n/l10n.dart';
import '../../../../shared/theme/app_design_tokens.dart';
import '../../../../shared/theme/app_palette.dart';
import '../../../../shared/widgets/app_tooltip.dart';
class TrackerSubscriptionsTable extends StatefulWidget {
const TrackerSubscriptionsTable({super.key, required this.urls, required this.statuses, required this.onDelete});
final List<String> urls;
final Map<String, TrackerSubscriptionStatus> statuses;
final ValueChanged<int> onDelete;
@override
State<TrackerSubscriptionsTable> createState() => _TrackerSubscriptionsTableState();
}
class _TrackerSubscriptionsTableState extends State<TrackerSubscriptionsTable> {
static const _headerHeight = 40.0;
static const _rowHeight = 48.0;
static const _statusWidth = 110.0;
static const _countWidth = 120.0;
static const _actionsWidth = 80.0;
final _horizontalController = ScrollController();
final _verticalController = ScrollController();
@override
void dispose() {
_horizontalController.dispose();
_verticalController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final palette = AppPalette.of(context);
final height = _headerHeight + _rowHeight * widget.urls.length.clamp(1, 5) + 12;
return Container(
key: const ValueKey('tracker-subscriptions-table'),
height: height,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
border: Border.all(color: palette.border),
borderRadius: BorderRadius.circular(AppDesignTokens.controlRadius),
),
child: LayoutBuilder(
builder: (context, constraints) {
final contentWidth = math.max(640.0, constraints.maxWidth);
final hiddenWidth = contentWidth - constraints.maxWidth;
return Scrollbar(
key: const ValueKey('tracker-subscriptions-horizontal-scrollbar'),
controller: _horizontalController,
interactive: true,
thickness: 7,
scrollbarOrientation: ScrollbarOrientation.bottom,
notificationPredicate: (notification) => notification.metrics.axis == Axis.horizontal,
child: Padding(
padding: const EdgeInsets.only(bottom: 12),
child: SingleChildScrollView(
controller: _horizontalController,
scrollDirection: Axis.horizontal,
physics: const ClampingScrollPhysics(),
child: SizedBox(
width: contentWidth,
child: Column(
children: [
Container(
height: _headerHeight,
decoration: BoxDecoration(
color: palette.sideBg,
border: Border(bottom: BorderSide(color: palette.border)),
),
child: Stack(
children: [
Positioned.fill(
child: Row(
children: [
Expanded(child: _header(context.l10n.trackerSubscriptionUrl)),
SizedBox(
width: _statusWidth,
child: _header(context.l10n.trackerSubscriptionUpdateStatus),
),
SizedBox(
width: _countWidth,
child: _header(context.l10n.trackerSubscriptionTrackerCount),
),
const SizedBox(width: _actionsWidth),
],
),
),
_pinnedActions(
hiddenWidth,
color: palette.sideBg,
child: SizedBox(
key: const ValueKey('tracker-subscriptions-actions-header'),
child: _header(context.l10n.actions),
),
),
],
),
),
Expanded(
child: widget.urls.isEmpty
? Center(
child: Text('—', style: TextStyle(color: palette.textMuted)),
)
: Scrollbar(
controller: _verticalController,
thumbVisibility: true,
notificationPredicate: (notification) => notification.metrics.axis == Axis.vertical,
child: ListView.builder(
key: const ValueKey('tracker-subscriptions-rows'),
controller: _verticalController,
padding: EdgeInsets.zero,
itemExtent: _rowHeight,
itemCount: widget.urls.length,
itemBuilder: (context, index) => _row(index, hiddenWidth),
),
),
),
],
),
),
),
),
);
},
),
);
}
Widget _header(String label) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: AppPalette.of(context).textSecondary, fontSize: 11, fontWeight: FontWeight.w700),
),
);
}
// Keep actions inside the same vertical list while compensating for the
// horizontal viewport offset, so each button stays aligned with its source.
Widget _pinnedActions(double hiddenWidth, {required Color color, required Widget child}) {
final palette = AppPalette.of(context);
return AnimatedBuilder(
animation: _horizontalController,
child: DecoratedBox(
decoration: BoxDecoration(
color: color,
border: Border(bottom: BorderSide(color: palette.border)),
),
child: Center(child: child),
),
builder: (context, child) => PositionedDirectional(
top: 0,
bottom: 0,
end:
hiddenWidth - (_horizontalController.hasClients ? _horizontalController.offset.clamp(0.0, hiddenWidth) : 0),
width: _actionsWidth,
child: child!,
),
);
}
Widget _row(int index, double hiddenWidth) {
final palette = AppPalette.of(context);
final url = widget.urls[index];
final status = widget.statuses[url];
final result = status == null
? context.l10n.trackerSubscriptionNotUpdated
: status.success
? context.l10n.trackerSubscriptionFetchSuccess
: context.l10n.trackerSubscriptionFetchFailed;
return DecoratedBox(
key: ValueKey('tracker-subscription-$url'),
decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: palette.border)),
),
child: Stack(
children: [
Positioned.fill(
child: Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: AppTooltip(
message: url,
child: Text(
url,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: palette.textPrimary, fontSize: 12),
),
),
),
),
SizedBox(
width: _statusWidth,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Text(
result,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: status == null
? palette.textMuted
: status.success
? palette.success
: palette.error,
fontSize: 11,
),
),
),
),
SizedBox(
width: _countWidth,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Text(
status?.trackerCount.toString() ?? '—',
style: TextStyle(color: palette.textSecondary, fontSize: 11),
),
),
),
const SizedBox(width: _actionsWidth),
],
),
),
_pinnedActions(
hiddenWidth,
color: palette.cardBg,
child: AppTooltip(
message: context.l10n.delete,
child: SizedBox.square(
dimension: 44,
child: GhostButton(
key: ValueKey('delete-settings-list-$url'),
density: ButtonDensity.icon,
onPressed: () => widget.onDelete(index),
child: const Icon(Icons.delete_outline, size: 17),
),
),
),
),
],
),
);
}
}