mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-15 04:43:41 +00:00
refactor: remove proxy API (#1596)
This commit is contained in:
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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
+106
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+56
@@ -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)}';
|
||||
}
|
||||
}
|
||||
+258
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user