mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-13 03:43:41 +00:00
feat: add MCP support (#1471)
This commit is contained in:
83 files changed
+3119
-363
No files matched your search
@@ -32,11 +32,18 @@ class DownloaderConfig {
|
||||
@JsonSerializable()
|
||||
class ApiServerConfig {
|
||||
bool enable;
|
||||
bool mcpEnable;
|
||||
String network;
|
||||
String address;
|
||||
String token;
|
||||
|
||||
ApiServerConfig({this.enable = false, this.network = 'tcp', this.address = '127.0.0.1:9999', this.token = ''});
|
||||
ApiServerConfig({
|
||||
this.enable = false,
|
||||
this.mcpEnable = false,
|
||||
this.network = 'tcp',
|
||||
this.address = '127.0.0.1:9999',
|
||||
this.token = '',
|
||||
});
|
||||
|
||||
factory ApiServerConfig.fromJson(Map<String, dynamic>? json) =>
|
||||
json == null ? ApiServerConfig() : _$ApiServerConfigFromJson(json);
|
||||
|
||||
@@ -39,6 +39,7 @@ Map<String, dynamic> _$DownloaderConfigToJson(DownloaderConfig instance) => <Str
|
||||
|
||||
ApiServerConfig _$ApiServerConfigFromJson(Map<String, dynamic> json) => ApiServerConfig(
|
||||
enable: json['enable'] as bool? ?? false,
|
||||
mcpEnable: json['mcpEnable'] as bool? ?? false,
|
||||
network: json['network'] as String? ?? 'tcp',
|
||||
address: json['address'] as String? ?? '127.0.0.1:9999',
|
||||
token: json['token'] as String? ?? '',
|
||||
@@ -46,6 +47,7 @@ ApiServerConfig _$ApiServerConfigFromJson(Map<String, dynamic> json) => ApiServe
|
||||
|
||||
Map<String, dynamic> _$ApiServerConfigToJson(ApiServerConfig instance) => <String, dynamic>{
|
||||
'enable': instance.enable,
|
||||
'mcpEnable': instance.mcpEnable,
|
||||
'network': instance.network,
|
||||
'address': instance.address,
|
||||
'token': instance.token,
|
||||
|
||||
@@ -13,6 +13,7 @@ import '../../core/common/start_config.dart';
|
||||
import '../../core/common/task_event.dart';
|
||||
import '../../core/entry/app_initializer.dart';
|
||||
import '../../core/libgopeed_boot.dart';
|
||||
import '../../core/network/gopeed/gopeed_transport.dart';
|
||||
import '../../features/auth/application/web_auth_controller.dart';
|
||||
import '../../l10n/l10n.dart';
|
||||
import '../../util/log_util.dart';
|
||||
@@ -64,11 +65,22 @@ class AppRuntimeController extends AsyncNotifier<AppRuntimeState> {
|
||||
if (current == null) return;
|
||||
final config = await ref.read(gopeedServiceProvider).getConfig();
|
||||
final apiState = kIsWeb ? current.apiServerState : (await LibgopeedBoot.instance.getApiServerState()).state;
|
||||
final startConfig = _copyStartConfig(current.startConfig)
|
||||
..apiEnable = config.api.enable
|
||||
..network = config.api.network
|
||||
..address = config.api.address
|
||||
..apiToken = config.api.token;
|
||||
final startConfig = _copyStartConfig(current.startConfig);
|
||||
if (kIsWeb) {
|
||||
// The Web client connection may point at a separate development server
|
||||
// from the page itself. Downloader config describes the remote process
|
||||
// and must not replace that client-side endpoint after reload.
|
||||
startConfig
|
||||
..apiEnable = true
|
||||
..mcpEnable = config.api.mcpEnable;
|
||||
} else {
|
||||
startConfig
|
||||
..apiEnable = config.api.enable
|
||||
..mcpEnable = config.api.mcpEnable
|
||||
..network = config.api.network
|
||||
..address = config.api.address
|
||||
..apiToken = config.api.token;
|
||||
}
|
||||
state = AsyncValue.data(
|
||||
AppRuntimeState(
|
||||
startConfig: startConfig,
|
||||
@@ -112,6 +124,7 @@ class AppRuntimeController extends AsyncNotifier<AppRuntimeState> {
|
||||
var runningPort = 0;
|
||||
var apiServerState = const ApiServerState(
|
||||
enabled: false,
|
||||
mcpEnabled: false,
|
||||
running: false,
|
||||
network: '',
|
||||
address: '',
|
||||
@@ -141,7 +154,7 @@ class AppRuntimeController extends AsyncNotifier<AppRuntimeState> {
|
||||
|
||||
if (kIsWeb) {
|
||||
final webAuth = ref.read(webAuthControllerProvider);
|
||||
api.init('tcp', Uri.base.authority, '', onUnauthorized: webAuth.requireLogin);
|
||||
api.init('tcp', startConfig.address, '', onUnauthorized: webAuth.requireLogin);
|
||||
} else {
|
||||
api.init(startConfig.network, _runningAddress(startConfig, runningPort), startConfig.apiToken);
|
||||
}
|
||||
@@ -150,6 +163,7 @@ class AppRuntimeController extends AsyncNotifier<AppRuntimeState> {
|
||||
if (!kIsWeb) {
|
||||
startConfig
|
||||
..apiEnable = config.api.enable
|
||||
..mcpEnable = config.api.mcpEnable
|
||||
..network = config.api.network
|
||||
..address = config.api.address
|
||||
..apiToken = config.api.token;
|
||||
@@ -206,7 +220,7 @@ class AppRuntimeController extends AsyncNotifier<AppRuntimeState> {
|
||||
if (kIsWeb) {
|
||||
cfg
|
||||
..network = 'tcp'
|
||||
..address = Uri.base.authority;
|
||||
..address = webApiBaseUrl(Uri.base);
|
||||
_defaultStartConfig = cfg;
|
||||
return cfg;
|
||||
}
|
||||
@@ -404,6 +418,7 @@ class AppRuntimeController extends AsyncNotifier<AppRuntimeState> {
|
||||
final downloaderConfig = await ref.read(gopeedServiceProvider).getConfig();
|
||||
downloaderConfig.api
|
||||
..enable = nextConfig.apiEnable
|
||||
..mcpEnable = nextConfig.mcpEnable
|
||||
..network = nextConfig.network
|
||||
..address = nextConfig.address
|
||||
..token = nextConfig.apiToken;
|
||||
@@ -459,6 +474,7 @@ class AppRuntimeController extends AsyncNotifier<AppRuntimeState> {
|
||||
..network = config.network
|
||||
..address = config.address
|
||||
..apiEnable = config.apiEnable
|
||||
..mcpEnable = config.mcpEnable
|
||||
..storage = config.storage
|
||||
..storageDir = config.storageDir
|
||||
..refreshInterval = config.refreshInterval
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
class ApiServerState {
|
||||
const ApiServerState({
|
||||
required this.enabled,
|
||||
required this.mcpEnabled,
|
||||
required this.running,
|
||||
required this.network,
|
||||
required this.address,
|
||||
@@ -10,6 +11,7 @@ class ApiServerState {
|
||||
});
|
||||
|
||||
final bool enabled;
|
||||
final bool mcpEnabled;
|
||||
final bool running;
|
||||
final String network;
|
||||
final String address;
|
||||
@@ -20,6 +22,7 @@ class ApiServerState {
|
||||
factory ApiServerState.fromJson(Map<String, dynamic> json) {
|
||||
return ApiServerState(
|
||||
enabled: json['enabled'] as bool? ?? false,
|
||||
mcpEnabled: json['mcpEnabled'] as bool? ?? false,
|
||||
running: json['running'] as bool? ?? false,
|
||||
network: json['network'] as String? ?? '',
|
||||
address: json['address'] as String? ?? '',
|
||||
@@ -50,6 +53,7 @@ class ApiServerOperationResult {
|
||||
state: rawState == null
|
||||
? const ApiServerState(
|
||||
enabled: false,
|
||||
mcpEnabled: false,
|
||||
running: false,
|
||||
network: '',
|
||||
address: '',
|
||||
|
||||
@@ -20,6 +20,7 @@ class StartConfig {
|
||||
late String network;
|
||||
late String address;
|
||||
bool apiEnable = false;
|
||||
bool mcpEnable = false;
|
||||
late String storage;
|
||||
late String storageDir;
|
||||
late int refreshInterval;
|
||||
|
||||
@@ -23,6 +23,7 @@ StartConfig _$StartConfigFromJson(Map<String, dynamic> json) => StartConfig()
|
||||
..network = json['network'] as String
|
||||
..address = json['address'] as String
|
||||
..apiEnable = json['apiEnable'] as bool
|
||||
..mcpEnable = json['mcpEnable'] as bool? ?? false
|
||||
..storage = json['storage'] as String
|
||||
..storageDir = json['storageDir'] as String
|
||||
..refreshInterval = (json['refreshInterval'] as num).toInt()
|
||||
@@ -38,6 +39,7 @@ Map<String, dynamic> _$StartConfigToJson(StartConfig instance) =>
|
||||
'network': instance.network,
|
||||
'address': instance.address,
|
||||
'apiEnable': instance.apiEnable,
|
||||
'mcpEnable': instance.mcpEnable,
|
||||
'storage': instance.storage,
|
||||
'storageDir': instance.storageDir,
|
||||
'refreshInterval': instance.refreshInterval,
|
||||
|
||||
@@ -43,6 +43,7 @@ class LibgopeedBootBrowser implements LibgopeedBoot {
|
||||
const _unsupportedApiServerResult = ApiServerOperationResult(
|
||||
state: ApiServerState(
|
||||
enabled: false,
|
||||
mcpEnabled: false,
|
||||
running: false,
|
||||
network: '',
|
||||
address: '',
|
||||
|
||||
@@ -8,7 +8,9 @@ class RestGopeedTransport implements GopeedTransport {
|
||||
RestGopeedTransport(GopeedTransportConfig config)
|
||||
: _dio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: '${Uri.base.origin}/',
|
||||
// Keep release requests relative so deployments under a reverse
|
||||
// proxy path continue to work, matching the legacy Web client.
|
||||
baseUrl: webTransportBaseUrl(config.address, Uri.base),
|
||||
contentType: Headers.jsonContentType,
|
||||
sendTimeout: const Duration(seconds: 5),
|
||||
connectTimeout: const Duration(seconds: 5),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'entry/gopeed_transport_stub.dart'
|
||||
if (dart.library.html) 'entry/gopeed_transport_web.dart'
|
||||
@@ -18,6 +19,44 @@ class GopeedTransportConfig {
|
||||
final void Function()? onUnauthorized;
|
||||
}
|
||||
|
||||
const defaultWebApiPort = 9999;
|
||||
|
||||
/// Resolves the API base URL used by Flutter Web.
|
||||
///
|
||||
/// This preserves the legacy behavior: debug builds connect to the local
|
||||
/// backend on port 9999, while release builds use the page's server origin.
|
||||
String webApiBaseUrl(Uri pageUri, {bool? debugMode}) {
|
||||
final useDebugPort = debugMode ?? kDebugMode;
|
||||
if (useDebugPort) return 'http://127.0.0.1:$defaultWebApiPort/';
|
||||
return '${pageUri.origin}/';
|
||||
}
|
||||
|
||||
/// Returns the Dio base URL, preserving relative requests in release builds.
|
||||
String webTransportBaseUrl(String address, Uri pageUri, {bool? debugMode}) {
|
||||
final useDebugPort = debugMode ?? kDebugMode;
|
||||
return useDebugPort ? normalizeWebApiBaseUrl(address, pageUri) : '';
|
||||
}
|
||||
|
||||
/// Normalizes either an absolute API URL or a host:port address.
|
||||
String normalizeWebApiBaseUrl(String address, Uri pageUri) {
|
||||
var value = address.trim();
|
||||
if (value.isEmpty) return '${pageUri.origin}/';
|
||||
|
||||
final parsed = Uri.tryParse(value);
|
||||
if (parsed != null && parsed.hasScheme && parsed.host.isNotEmpty) {
|
||||
value = Uri(
|
||||
scheme: parsed.scheme,
|
||||
userInfo: parsed.userInfo,
|
||||
host: parsed.host,
|
||||
port: parsed.hasPort ? parsed.port : null,
|
||||
path: parsed.path,
|
||||
).toString();
|
||||
} else {
|
||||
value = '${pageUri.scheme}://${value.replaceAll(RegExp(r'/+$'), '')}';
|
||||
}
|
||||
return value.endsWith('/') ? value : '$value/';
|
||||
}
|
||||
|
||||
abstract interface class GopeedTransport {
|
||||
Future<dynamic> request(String path, {String method = 'GET', dynamic data, Map<String, dynamic>? queryParameters});
|
||||
|
||||
|
||||
@@ -10,7 +10,14 @@ abstract final class AppWindowChrome {
|
||||
static bool get usesCustomChrome =>
|
||||
!kIsWeb && (defaultTargetPlatform == TargetPlatform.windows || defaultTargetPlatform == TargetPlatform.linux);
|
||||
|
||||
static bool get reservesHeaderInset => isDesktopWindow;
|
||||
static bool get reservesHeaderInset => reservesHeaderInsetFor(web: kIsWeb, platform: defaultTargetPlatform);
|
||||
|
||||
static bool reservesHeaderInsetFor({required bool web, required TargetPlatform platform}) {
|
||||
return web ||
|
||||
platform == TargetPlatform.windows ||
|
||||
platform == TargetPlatform.macOS ||
|
||||
platform == TargetPlatform.linux;
|
||||
}
|
||||
|
||||
static bool get clipsRoundedCorners => !kIsWeb && defaultTargetPlatform == TargetPlatform.windows;
|
||||
}
|
||||
@@ -696,14 +696,11 @@ class _Toolbar extends StatelessWidget {
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: ConstrainedBox(
|
||||
key: const ValueKey('extension-search-field-container'),
|
||||
constraints: const BoxConstraints(maxWidth: 240),
|
||||
child: search,
|
||||
),
|
||||
Flexible(
|
||||
child: ConstrainedBox(
|
||||
key: const ValueKey('extension-search-field-container'),
|
||||
constraints: const BoxConstraints(maxWidth: 240),
|
||||
child: search,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
|
||||
@@ -60,7 +60,7 @@ class TasksTopBar extends StatelessWidget {
|
||||
const SizedBox(width: 16),
|
||||
ConstrainedBox(
|
||||
key: const ValueKey('tasks-create-button-container'),
|
||||
constraints: const BoxConstraints(maxWidth: 190),
|
||||
constraints: const BoxConstraints(minWidth: 120, maxWidth: 190),
|
||||
child: _AddTaskButton(onPressed: onAddTask),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
@@ -139,8 +139,14 @@ class _AddTaskButton extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return AppPrimaryButton(
|
||||
onPressed: onPressed,
|
||||
leading: const Icon(Icons.add),
|
||||
child: Text(context.l10n.create, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.add),
|
||||
const SizedBox(width: AppDesignTokens.space8),
|
||||
Flexible(child: Text(context.l10n.create, maxLines: 1, overflow: TextOverflow.ellipsis)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+8
-2
@@ -64,8 +64,14 @@ class DownloadCategoriesControl extends StatelessWidget {
|
||||
child: shad.SecondaryButton(
|
||||
key: const ValueKey('add-download-category'),
|
||||
onPressed: onAdd,
|
||||
leading: const Icon(Icons.add, size: 17),
|
||||
child: Text(context.l10n.add),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.add, size: 17),
|
||||
const SizedBox(width: AppDesignTokens.space8),
|
||||
Text(context.l10n.add),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -5,10 +5,19 @@ import '../../../../shared/theme/app_design_tokens.dart';
|
||||
import '../../../../shared/theme/app_palette.dart';
|
||||
|
||||
class SettingsItem extends StatelessWidget {
|
||||
const SettingsItem({super.key, required this.title, required this.child, this.subtitle});
|
||||
const SettingsItem({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.child,
|
||||
this.subtitle,
|
||||
this.subtitleAction,
|
||||
this.titleTrailing,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final Widget? subtitleAction;
|
||||
final Widget? titleTrailing;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
@@ -29,7 +38,12 @@ class SettingsItem extends StatelessWidget {
|
||||
children: [
|
||||
const SizedBox(width: leadingSpace),
|
||||
Expanded(
|
||||
child: _SettingsItemLabel(title: title, subtitle: subtitle),
|
||||
child: _SettingsItemLabel(
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
subtitleAction: subtitleAction,
|
||||
titleTrailing: titleTrailing,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: columnGap),
|
||||
ConstrainedBox(
|
||||
@@ -45,7 +59,12 @@ class SettingsItem extends StatelessWidget {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_SettingsItemLabel(title: title, subtitle: subtitle),
|
||||
_SettingsItemLabel(
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
subtitleAction: subtitleAction,
|
||||
titleTrailing: titleTrailing,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Align(alignment: Alignment.centerLeft, child: child),
|
||||
],
|
||||
@@ -54,10 +73,12 @@ class SettingsItem extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _SettingsItemLabel extends StatelessWidget {
|
||||
const _SettingsItemLabel({required this.title, this.subtitle});
|
||||
const _SettingsItemLabel({required this.title, this.subtitle, this.subtitleAction, this.titleTrailing});
|
||||
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final Widget? subtitleAction;
|
||||
final Widget? titleTrailing;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -66,13 +87,30 @@ class _SettingsItemLabel extends StatelessWidget {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(color: palette.textPrimary, fontSize: 13, fontWeight: FontWeight.w700),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(color: palette.textPrimary, fontSize: 13, fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
if (titleTrailing != null) ...[const SizedBox(width: 5), titleTrailing!],
|
||||
],
|
||||
),
|
||||
if (subtitleText != null && subtitleText.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(subtitleText, style: TextStyle(color: palette.textSecondary, fontSize: 12, height: 1.3)),
|
||||
Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
spacing: 6,
|
||||
runSpacing: 2,
|
||||
children: [
|
||||
Text(subtitleText, style: TextStyle(color: palette.textSecondary, fontSize: 12, height: 1.3)),
|
||||
?subtitleAction,
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
@@ -99,8 +99,14 @@ class SettingsListEditor extends StatelessWidget {
|
||||
child: shad.SecondaryButton(
|
||||
key: addButtonKey,
|
||||
onPressed: onAdd,
|
||||
leading: const Icon(Icons.add, size: 17),
|
||||
child: Text(addLabel ?? context.l10n.add),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.add, size: 17),
|
||||
const SizedBox(width: AppDesignTokens.space8),
|
||||
Text(addLabel ?? context.l10n.add),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -280,8 +286,16 @@ Future<GithubMirrorDraft?> showGithubMirrorDialog(BuildContext context, {GithubM
|
||||
for (final type in GithubMirrorType.values)
|
||||
shad.SecondaryButton(
|
||||
onPressed: () => setDialogState(() => selectedType = type),
|
||||
leading: selectedType == type ? const Icon(Icons.check, size: 15) : null,
|
||||
child: Text(type.name),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (selectedType == type) ...[
|
||||
const Icon(Icons.check, size: 15),
|
||||
const SizedBox(width: AppDesignTokens.space8),
|
||||
],
|
||||
Text(type.name),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -3,12 +3,12 @@ import 'dart:async';
|
||||
import 'package:flutter/foundation.dart' show TargetPlatform, defaultTargetPlatform, kIsWeb;
|
||||
import 'package:flutter/material.dart' show Colors, Icons;
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
|
||||
|
||||
import '../../../../shared/theme/app_design_tokens.dart';
|
||||
import '../../../../shared/theme/app_palette.dart';
|
||||
import '../../../../shared/widgets/app_copy_icon_button.dart';
|
||||
import '../../../../shared/widgets/app_tooltip.dart';
|
||||
import '../../../../shared/widgets/detail/app_detail_surface.dart';
|
||||
import '../../../../core/utils/text_wrap.dart';
|
||||
@@ -81,8 +81,6 @@ class TaskDetailsView extends ConsumerStatefulWidget {
|
||||
class _TaskDetailsViewState extends ConsumerState<TaskDetailsView> {
|
||||
final _urlController = TextEditingController();
|
||||
int _tabIndex = 0;
|
||||
bool _copied = false;
|
||||
bool _storagePathCopied = false;
|
||||
bool _editingUrl = false;
|
||||
bool _updatingUrl = false;
|
||||
|
||||
@@ -97,8 +95,6 @@ class _TaskDetailsViewState extends ConsumerState<TaskDetailsView> {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.task.id != oldWidget.task.id) {
|
||||
_tabIndex = 0;
|
||||
_copied = false;
|
||||
_storagePathCopied = false;
|
||||
_editingUrl = false;
|
||||
_updatingUrl = false;
|
||||
_urlController.text = widget.task.url;
|
||||
@@ -171,8 +167,6 @@ class _TaskDetailsViewState extends ConsumerState<TaskDetailsView> {
|
||||
padding: EdgeInsets.all(contentPadding),
|
||||
child: _TaskInfoTab(
|
||||
task: widget.task,
|
||||
copied: _copied,
|
||||
storagePathCopied: _storagePathCopied,
|
||||
canOpenStorage: _canOpenStoragePath,
|
||||
editingUrl: _editingUrl,
|
||||
updatingUrl: _updatingUrl,
|
||||
@@ -189,14 +183,6 @@ class _TaskDetailsViewState extends ConsumerState<TaskDetailsView> {
|
||||
_editingUrl = false;
|
||||
}),
|
||||
onSaveUrl: () => unawaited(_saveUrl()),
|
||||
onCopy: () async {
|
||||
await Clipboard.setData(ClipboardData(text: widget.task.url));
|
||||
if (mounted) setState(() => _copied = true);
|
||||
},
|
||||
onCopyStorage: () async {
|
||||
await Clipboard.setData(ClipboardData(text: widget.task.storagePath));
|
||||
if (mounted) setState(() => _storagePathCopied = true);
|
||||
},
|
||||
),
|
||||
),
|
||||
1 => Padding(
|
||||
@@ -281,14 +267,10 @@ class _DrawerTabButton extends StatelessWidget {
|
||||
class _TaskInfoTab extends StatelessWidget {
|
||||
const _TaskInfoTab({
|
||||
required this.task,
|
||||
required this.copied,
|
||||
required this.storagePathCopied,
|
||||
required this.canOpenStorage,
|
||||
required this.editingUrl,
|
||||
required this.updatingUrl,
|
||||
required this.urlController,
|
||||
required this.onCopy,
|
||||
required this.onCopyStorage,
|
||||
required this.onOpenStorage,
|
||||
required this.onEditUrl,
|
||||
required this.onCancelEditUrl,
|
||||
@@ -296,14 +278,10 @@ class _TaskInfoTab extends StatelessWidget {
|
||||
});
|
||||
|
||||
final TaskRecord task;
|
||||
final bool copied;
|
||||
final bool storagePathCopied;
|
||||
final bool canOpenStorage;
|
||||
final bool editingUrl;
|
||||
final bool updatingUrl;
|
||||
final TextEditingController urlController;
|
||||
final VoidCallback onCopy;
|
||||
final VoidCallback onCopyStorage;
|
||||
final VoidCallback onOpenStorage;
|
||||
final VoidCallback? onEditUrl;
|
||||
final VoidCallback onCancelEditUrl;
|
||||
@@ -357,30 +335,23 @@ class _TaskInfoTab extends StatelessWidget {
|
||||
label: context.l10n.downloadLink,
|
||||
value: task.url,
|
||||
valueKey: const ValueKey('task-details-url-value'),
|
||||
actionKey: const ValueKey('task-details-copy-url'),
|
||||
actionLabel: copied ? context.l10n.copied : context.l10n.copy,
|
||||
actionIcon: copied ? Icons.check : Icons.copy_outlined,
|
||||
action: AppCopyIconButton(key: const ValueKey('task-details-copy-url'), text: task.url),
|
||||
secondaryActionLabel: onEditUrl == null ? null : context.l10n.edit,
|
||||
secondaryActionIcon: Icons.edit_outlined,
|
||||
onSecondaryTap: onEditUrl,
|
||||
onTap: onCopy,
|
||||
),
|
||||
_CopyableInfoBlock(
|
||||
label: context.l10n.storagePath,
|
||||
value: task.storagePath,
|
||||
valueKey: const ValueKey('task-details-storage-value'),
|
||||
actionKey: ValueKey(canOpenStorage ? 'task-details-open-storage' : 'task-details-copy-storage'),
|
||||
actionLabel: canOpenStorage
|
||||
? context.l10n.open
|
||||
: storagePathCopied
|
||||
? context.l10n.copied
|
||||
: context.l10n.copy,
|
||||
actionIcon: canOpenStorage
|
||||
? Icons.folder_open_outlined
|
||||
: storagePathCopied
|
||||
? Icons.check
|
||||
: Icons.copy_outlined,
|
||||
onTap: canOpenStorage ? onOpenStorage : onCopyStorage,
|
||||
action: canOpenStorage
|
||||
? _TaskDetailIconButton(
|
||||
key: const ValueKey('task-details-open-storage'),
|
||||
label: context.l10n.open,
|
||||
icon: const Icon(Icons.folder_open_outlined, size: 16),
|
||||
onPressed: onOpenStorage,
|
||||
)
|
||||
: AppCopyIconButton(key: const ValueKey('task-details-copy-storage'), text: task.storagePath),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -428,10 +399,7 @@ class _CopyableInfoBlock extends StatelessWidget {
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.valueKey,
|
||||
required this.actionKey,
|
||||
required this.actionLabel,
|
||||
required this.actionIcon,
|
||||
required this.onTap,
|
||||
required this.action,
|
||||
this.secondaryActionLabel,
|
||||
this.secondaryActionIcon,
|
||||
this.onSecondaryTap,
|
||||
@@ -440,10 +408,7 @@ class _CopyableInfoBlock extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
final Key valueKey;
|
||||
final Key actionKey;
|
||||
final String actionLabel;
|
||||
final IconData actionIcon;
|
||||
final VoidCallback onTap;
|
||||
final Widget action;
|
||||
final String? secondaryActionLabel;
|
||||
final IconData? secondaryActionIcon;
|
||||
final VoidCallback? onSecondaryTap;
|
||||
@@ -481,12 +446,7 @@ class _CopyableInfoBlock extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
_TaskDetailIconButton(
|
||||
key: actionKey,
|
||||
label: actionLabel,
|
||||
icon: Icon(actionIcon, size: 16),
|
||||
onPressed: onTap,
|
||||
),
|
||||
action,
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -432,5 +432,14 @@
|
||||
"booleanValueHint": "true / false",
|
||||
"androidForegroundServiceChannel": "خدمة Gopeed في الخلفية",
|
||||
"androidForegroundNotificationTitle": "Gopeed قيد التشغيل",
|
||||
"androidForegroundNotificationText": "يمكن متابعة التنزيلات في الخلفية"
|
||||
"androidForegroundNotificationText": "يمكن متابعة التنزيلات في الخلفية",
|
||||
"mcpDescription": "تعاون مع وكلاء الذكاء الاصطناعي واستكشف إمكانات بلا حدود",
|
||||
"connectAgent": "دليل الإعداد",
|
||||
"connectMcpAgent": "ربط وكيل MCP",
|
||||
"mcpAgentDialogDescription": "اختر الوكيل الذي تستخدمه، ثم انسخ إعدادات الاتصال الخاصة به.",
|
||||
"mcpNotRunningHint": "خدمة MCP غير مفعّلة. يرجى تفعيلها",
|
||||
"webApiConfigReadOnlyHelp": "لا يدعم إصدار الويب تعديل إعدادات API مباشرة. عدّل خيارات بدء تشغيل الخادم أو ملف الإعدادات، ثم أعد تشغيل الخدمة لتطبيق التغييرات.",
|
||||
"webMcpApiTokenHint": "إذا قمت بتكوين رمز API، فيجب تمرير معلومات المصادقة عند ربط الوكيل",
|
||||
"useMcpApiTokenTemplate": "التبديل إلى إعدادات المصادقة",
|
||||
"mcpRequiresTcp": "تتوفر إرشادات ربط الوكيل عندما يكون بروتوكول API هو TCP."
|
||||
}
|
||||
@@ -432,5 +432,14 @@
|
||||
"booleanValueHint": "true / false",
|
||||
"androidForegroundServiceChannel": "Servei en segon pla de Gopeed",
|
||||
"androidForegroundNotificationTitle": "Gopeed s'està executant",
|
||||
"androidForegroundNotificationText": "Les baixades poden continuar en segon pla"
|
||||
"androidForegroundNotificationText": "Les baixades poden continuar en segon pla",
|
||||
"mcpDescription": "Col·labora amb agents d’IA i explora possibilitats infinites",
|
||||
"connectAgent": "Guia de connexió",
|
||||
"connectMcpAgent": "Connecta un agent MCP",
|
||||
"mcpAgentDialogDescription": "Tria l’agent que utilitzes i copia’n la configuració de connexió.",
|
||||
"mcpNotRunningHint": "El servei MCP no està activat. Activa’l",
|
||||
"webApiConfigReadOnlyHelp": "La versió web no permet modificar la configuració de l’API en línia. Actualitza les opcions d’inici del servidor o el fitxer de configuració i reinicia el servei perquè els canvis tinguin efecte.",
|
||||
"webMcpApiTokenHint": "Si heu configurat un token d’API, cal proporcionar la informació d’autenticació en connectar l’agent",
|
||||
"useMcpApiTokenTemplate": "Canvia a la configuració autenticada",
|
||||
"mcpRequiresTcp": "Les instruccions de connexió d’agents estan disponibles quan el protocol de l’API és TCP."
|
||||
}
|
||||
@@ -432,5 +432,14 @@
|
||||
"booleanValueHint": "true / false",
|
||||
"androidForegroundServiceChannel": "Gopeed-Hintergrunddienst",
|
||||
"androidForegroundNotificationTitle": "Gopeed wird ausgeführt",
|
||||
"androidForegroundNotificationText": "Downloads können im Hintergrund fortgesetzt werden"
|
||||
"androidForegroundNotificationText": "Downloads können im Hintergrund fortgesetzt werden",
|
||||
"mcpDescription": "Mit KI-Agenten zusammenarbeiten und grenzenlose Möglichkeiten entdecken",
|
||||
"connectAgent": "Einrichtungsleitfaden",
|
||||
"connectMcpAgent": "MCP-Agent verbinden",
|
||||
"mcpAgentDialogDescription": "Wähle deinen Agenten aus und kopiere die zugehörige Verbindungskonfiguration.",
|
||||
"mcpNotRunningHint": "Der MCP-Dienst ist nicht aktiviert. Bitte aktiviere ihn",
|
||||
"webApiConfigReadOnlyHelp": "In der Web-Version können API-Einstellungen nicht online geändert werden. Ändere die Server-Startoptionen oder die Konfigurationsdatei und starte den Dienst neu, damit die Änderungen wirksam werden.",
|
||||
"webMcpApiTokenHint": "Wenn du ein API-Token konfiguriert hast, müssen beim Verbinden des Agents Authentifizierungsinformationen übermittelt werden",
|
||||
"useMcpApiTokenTemplate": "Zur authentifizierten Konfiguration wechseln",
|
||||
"mcpRequiresTcp": "Anweisungen zur Agent-Verbindung sind verfügbar, wenn das API-Protokoll TCP ist."
|
||||
}
|
||||
@@ -616,5 +616,14 @@
|
||||
},
|
||||
"androidForegroundServiceChannel": "Gopeed Background Service",
|
||||
"androidForegroundNotificationTitle": "Gopeed is running",
|
||||
"androidForegroundNotificationText": "Downloads can continue in the background"
|
||||
"androidForegroundNotificationText": "Downloads can continue in the background",
|
||||
"mcpDescription": "Collaborate with AI agents and explore limitless possibilities",
|
||||
"connectAgent": "Setup guide",
|
||||
"connectMcpAgent": "Connect MCP agent",
|
||||
"mcpAgentDialogDescription": "Choose the agent you use, then copy its connection configuration.",
|
||||
"mcpNotRunningHint": "The MCP service is not enabled. Please enable it",
|
||||
"webMcpApiTokenHint": "If you configured an API token, authentication information is required when connecting the agent",
|
||||
"useMcpApiTokenTemplate": "Switch to authenticated configuration",
|
||||
"webApiConfigReadOnlyHelp": "The web version cannot modify API settings online. Update the server startup options or configuration file, then restart the service for changes to take effect.",
|
||||
"mcpRequiresTcp": "Agent connection instructions are available when the API protocol is TCP."
|
||||
}
|
||||
@@ -432,5 +432,14 @@
|
||||
"booleanValueHint": "true / false",
|
||||
"androidForegroundServiceChannel": "Servicio en segundo plano de Gopeed",
|
||||
"androidForegroundNotificationTitle": "Gopeed se está ejecutando",
|
||||
"androidForegroundNotificationText": "Las descargas pueden continuar en segundo plano"
|
||||
"androidForegroundNotificationText": "Las descargas pueden continuar en segundo plano",
|
||||
"mcpDescription": "Colabora con agentes de IA y explora posibilidades ilimitadas",
|
||||
"connectAgent": "Guía de conexión",
|
||||
"connectMcpAgent": "Conectar agente MCP",
|
||||
"mcpAgentDialogDescription": "Elige el agente que utilizas y copia su configuración de conexión.",
|
||||
"mcpNotRunningHint": "El servicio MCP no está habilitado. Habilítalo",
|
||||
"webApiConfigReadOnlyHelp": "La versión web no permite modificar la configuración de la API en línea. Actualiza las opciones de inicio del servidor o el archivo de configuración y reinicia el servicio para aplicar los cambios.",
|
||||
"webMcpApiTokenHint": "Si has configurado un token de API, debes proporcionar la información de autenticación al conectar el agente",
|
||||
"useMcpApiTokenTemplate": "Cambiar a la configuración autenticada",
|
||||
"mcpRequiresTcp": "Las instrucciones de conexión de agentes están disponibles cuando el protocolo de la API es TCP."
|
||||
}
|
||||
@@ -432,5 +432,14 @@
|
||||
"booleanValueHint": "true / false",
|
||||
"androidForegroundServiceChannel": "سرویس پسزمینه Gopeed",
|
||||
"androidForegroundNotificationTitle": "Gopeed در حال اجرا است",
|
||||
"androidForegroundNotificationText": "دانلودها میتوانند در پسزمینه ادامه پیدا کنند"
|
||||
"androidForegroundNotificationText": "دانلودها میتوانند در پسزمینه ادامه پیدا کنند",
|
||||
"mcpDescription": "با عاملهای هوش مصنوعی همکاری کنید و امکانهای نامحدود را کشف کنید",
|
||||
"connectAgent": "راهنمای اتصال",
|
||||
"connectMcpAgent": "اتصال عامل MCP",
|
||||
"mcpAgentDialogDescription": "عامل مورد استفاده را انتخاب کنید و پیکربندی اتصال آن را کپی کنید.",
|
||||
"mcpNotRunningHint": "سرویس MCP فعال نیست. لطفاً آن را فعال کنید",
|
||||
"webApiConfigReadOnlyHelp": "نسخه وب از ویرایش آنلاین تنظیمات API پشتیبانی نمیکند. گزینههای راهاندازی سرور یا فایل پیکربندی را تغییر دهید و برای اعمال تغییرات سرویس را دوباره راهاندازی کنید.",
|
||||
"webMcpApiTokenHint": "اگر توکن API را پیکربندی کردهاید، هنگام اتصال عامل باید اطلاعات احراز هویت را ارسال کنید",
|
||||
"useMcpApiTokenTemplate": "تغییر به پیکربندی احراز هویت",
|
||||
"mcpRequiresTcp": "راهنمای اتصال عامل زمانی در دسترس است که پروتکل API روی TCP باشد."
|
||||
}
|
||||
@@ -432,5 +432,14 @@
|
||||
"booleanValueHint": "true / false",
|
||||
"androidForegroundServiceChannel": "Service d’arrière-plan Gopeed",
|
||||
"androidForegroundNotificationTitle": "Gopeed est en cours d’exécution",
|
||||
"androidForegroundNotificationText": "Les téléchargements peuvent continuer en arrière-plan"
|
||||
"androidForegroundNotificationText": "Les téléchargements peuvent continuer en arrière-plan",
|
||||
"mcpDescription": "Collaborez avec des agents IA et explorez des possibilités infinies",
|
||||
"connectAgent": "Guide de connexion",
|
||||
"connectMcpAgent": "Connecter un agent MCP",
|
||||
"mcpAgentDialogDescription": "Choisissez votre agent, puis copiez sa configuration de connexion.",
|
||||
"mcpNotRunningHint": "Le service MCP n’est pas activé. Activez-le",
|
||||
"webApiConfigReadOnlyHelp": "La version web ne permet pas de modifier les paramètres de l’API en ligne. Modifiez les options de démarrage du serveur ou le fichier de configuration, puis redémarrez le service pour appliquer les changements.",
|
||||
"webMcpApiTokenHint": "Si vous avez configuré un jeton API, les informations d’authentification doivent être transmises lors de la connexion de l’agent",
|
||||
"useMcpApiTokenTemplate": "Passer à la configuration authentifiée",
|
||||
"mcpRequiresTcp": "Les instructions de connexion des agents sont disponibles lorsque le protocole de l’API est TCP."
|
||||
}
|
||||
@@ -432,5 +432,14 @@
|
||||
"booleanValueHint": "true / false",
|
||||
"androidForegroundServiceChannel": "Gopeed háttérszolgáltatás",
|
||||
"androidForegroundNotificationTitle": "A Gopeed fut",
|
||||
"androidForegroundNotificationText": "A letöltések a háttérben is folytatódhatnak"
|
||||
"androidForegroundNotificationText": "A letöltések a háttérben is folytatódhatnak",
|
||||
"mcpDescription": "Működj együtt AI-ügynökökkel, és fedezz fel korlátlan lehetőségeket",
|
||||
"connectAgent": "Csatlakoztatási útmutató",
|
||||
"connectMcpAgent": "MCP-ügynök csatlakoztatása",
|
||||
"mcpAgentDialogDescription": "Válaszd ki a használt ügynököt, majd másold ki a kapcsolódási beállításait.",
|
||||
"mcpNotRunningHint": "Az MCP szolgáltatás nincs engedélyezve. Engedélyezd",
|
||||
"webApiConfigReadOnlyHelp": "A webes verzióban az API-beállítások nem módosíthatók online. Módosítsd a kiszolgáló indítási beállításait vagy a konfigurációs fájlt, majd indítsd újra a szolgáltatást.",
|
||||
"webMcpApiTokenHint": "Ha API-tokent állítottál be, az ügynök csatlakoztatásakor meg kell adni a hitelesítési adatokat",
|
||||
"useMcpApiTokenTemplate": "Váltás hitelesített konfigurációra",
|
||||
"mcpRequiresTcp": "Az ügynökcsatlakoztatási útmutató akkor érhető el, ha az API protokollja TCP."
|
||||
}
|
||||
@@ -432,5 +432,14 @@
|
||||
"booleanValueHint": "true / false",
|
||||
"androidForegroundServiceChannel": "Layanan latar belakang Gopeed",
|
||||
"androidForegroundNotificationTitle": "Gopeed sedang berjalan",
|
||||
"androidForegroundNotificationText": "Unduhan dapat dilanjutkan di latar belakang"
|
||||
"androidForegroundNotificationText": "Unduhan dapat dilanjutkan di latar belakang",
|
||||
"mcpDescription": "Berkolaborasi dengan agen AI dan jelajahi kemungkinan tanpa batas",
|
||||
"connectAgent": "Panduan koneksi",
|
||||
"connectMcpAgent": "Hubungkan agen MCP",
|
||||
"mcpAgentDialogDescription": "Pilih agen yang Anda gunakan, lalu salin konfigurasi koneksinya.",
|
||||
"mcpNotRunningHint": "Layanan MCP belum diaktifkan. Silakan aktifkan",
|
||||
"webApiConfigReadOnlyHelp": "Versi web tidak dapat mengubah pengaturan API secara online. Perbarui opsi startup server atau file konfigurasi, lalu mulai ulang layanan agar perubahan diterapkan.",
|
||||
"webMcpApiTokenHint": "Jika Anda telah mengatur token API, informasi autentikasi harus disertakan saat menghubungkan agen",
|
||||
"useMcpApiTokenTemplate": "Beralih ke konfigurasi terautentikasi",
|
||||
"mcpRequiresTcp": "Petunjuk koneksi agen tersedia saat protokol API menggunakan TCP."
|
||||
}
|
||||
@@ -432,5 +432,14 @@
|
||||
"booleanValueHint": "true / false",
|
||||
"androidForegroundServiceChannel": "Servizio in background Gopeed",
|
||||
"androidForegroundNotificationTitle": "Gopeed è in esecuzione",
|
||||
"androidForegroundNotificationText": "I download possono continuare in background"
|
||||
"androidForegroundNotificationText": "I download possono continuare in background",
|
||||
"mcpDescription": "Collabora con gli agenti IA ed esplora possibilità infinite",
|
||||
"connectAgent": "Guida alla connessione",
|
||||
"connectMcpAgent": "Connetti agente MCP",
|
||||
"mcpAgentDialogDescription": "Scegli l’agente che utilizzi e copia la relativa configurazione di connessione.",
|
||||
"mcpNotRunningHint": "Il servizio MCP non è abilitato. Abilitalo",
|
||||
"webApiConfigReadOnlyHelp": "La versione web non consente di modificare online le impostazioni API. Aggiorna le opzioni di avvio del server o il file di configurazione, quindi riavvia il servizio per applicare le modifiche.",
|
||||
"webMcpApiTokenHint": "Se hai configurato un token API, devi fornire le informazioni di autenticazione quando colleghi l’agente",
|
||||
"useMcpApiTokenTemplate": "Passa alla configurazione autenticata",
|
||||
"mcpRequiresTcp": "Le istruzioni per connettere gli agenti sono disponibili quando il protocollo API è TCP."
|
||||
}
|
||||
@@ -432,5 +432,14 @@
|
||||
"booleanValueHint": "true / false",
|
||||
"androidForegroundServiceChannel": "Gopeed バックグラウンドサービス",
|
||||
"androidForegroundNotificationTitle": "Gopeed は実行中です",
|
||||
"androidForegroundNotificationText": "バックグラウンドでダウンロードを続行できます"
|
||||
"androidForegroundNotificationText": "バックグラウンドでダウンロードを続行できます",
|
||||
"mcpDescription": "AI エージェントと連携し、無限の可能性を探る",
|
||||
"connectAgent": "接続ガイド",
|
||||
"connectMcpAgent": "MCP エージェントを接続",
|
||||
"mcpAgentDialogDescription": "使用するエージェントを選択し、接続設定をコピーしてください。",
|
||||
"mcpNotRunningHint": "MCP サービスが有効になっていません。有効にしてください",
|
||||
"webApiConfigReadOnlyHelp": "Web 版では API 設定をオンラインで変更できません。サーバーの起動オプションまたは設定ファイルを変更し、サービスを再起動して反映してください。",
|
||||
"webMcpApiTokenHint": "API トークンを設定している場合、エージェント接続時に認証情報を渡す必要があります",
|
||||
"useMcpApiTokenTemplate": "認証付き設定に切り替える",
|
||||
"mcpRequiresTcp": "API プロトコルが TCP の場合にエージェント接続手順を利用できます。"
|
||||
}
|
||||
@@ -432,5 +432,14 @@
|
||||
"booleanValueHint": "true / false",
|
||||
"androidForegroundServiceChannel": "Gopeed 백그라운드 서비스",
|
||||
"androidForegroundNotificationTitle": "Gopeed가 실행 중입니다",
|
||||
"androidForegroundNotificationText": "백그라운드에서 다운로드를 계속할 수 있습니다"
|
||||
"androidForegroundNotificationText": "백그라운드에서 다운로드를 계속할 수 있습니다",
|
||||
"mcpDescription": "AI 에이전트와 협업해 무한한 가능성 탐색",
|
||||
"connectAgent": "연결 가이드",
|
||||
"connectMcpAgent": "MCP 에이전트 연결",
|
||||
"mcpAgentDialogDescription": "사용할 에이전트를 선택한 다음 연결 구성을 복사하세요.",
|
||||
"mcpNotRunningHint": "MCP 서비스가 활성화되지 않았습니다. 활성화하세요",
|
||||
"webApiConfigReadOnlyHelp": "웹 버전에서는 API 설정을 온라인으로 수정할 수 없습니다. 서버 시작 옵션이나 구성 파일을 변경한 후 서비스를 다시 시작해야 적용됩니다.",
|
||||
"webMcpApiTokenHint": "API 토큰을 설정한 경우 에이전트 연결 시 인증 정보를 전달해야 합니다",
|
||||
"useMcpApiTokenTemplate": "인증 구성으로 전환",
|
||||
"mcpRequiresTcp": "API 프로토콜이 TCP일 때 에이전트 연결 안내를 사용할 수 있습니다."
|
||||
}
|
||||
@@ -432,5 +432,14 @@
|
||||
"booleanValueHint": "true / false",
|
||||
"androidForegroundServiceChannel": "Usługa Gopeed w tle",
|
||||
"androidForegroundNotificationTitle": "Gopeed jest uruchomiony",
|
||||
"androidForegroundNotificationText": "Pobieranie może być kontynuowane w tle"
|
||||
"androidForegroundNotificationText": "Pobieranie może być kontynuowane w tle",
|
||||
"mcpDescription": "Współpracuj z agentami AI i odkrywaj nieograniczone możliwości",
|
||||
"connectAgent": "Przewodnik konfiguracji",
|
||||
"connectMcpAgent": "Połącz agenta MCP",
|
||||
"mcpAgentDialogDescription": "Wybierz używanego agenta, a następnie skopiuj jego konfigurację połączenia.",
|
||||
"mcpNotRunningHint": "Usługa MCP nie jest włączona. Włącz ją",
|
||||
"webApiConfigReadOnlyHelp": "Wersja webowa nie obsługuje edycji ustawień API online. Zmień opcje uruchamiania serwera lub plik konfiguracyjny, a następnie uruchom usługę ponownie.",
|
||||
"webMcpApiTokenHint": "Jeśli skonfigurowano token API, podczas łączenia agenta należy przekazać dane uwierzytelniające",
|
||||
"useMcpApiTokenTemplate": "Przełącz na konfigurację z uwierzytelnianiem",
|
||||
"mcpRequiresTcp": "Instrukcje łączenia agentów są dostępne, gdy protokół API to TCP."
|
||||
}
|
||||
@@ -432,5 +432,14 @@
|
||||
"booleanValueHint": "true / false",
|
||||
"androidForegroundServiceChannel": "Serviço em segundo plano do Gopeed",
|
||||
"androidForegroundNotificationTitle": "O Gopeed está em execução",
|
||||
"androidForegroundNotificationText": "Os downloads podem continuar em segundo plano"
|
||||
"androidForegroundNotificationText": "Os downloads podem continuar em segundo plano",
|
||||
"mcpDescription": "Colabore com agentes de IA e explore possibilidades ilimitadas",
|
||||
"connectAgent": "Guia de conexão",
|
||||
"connectMcpAgent": "Conectar agente MCP",
|
||||
"mcpAgentDialogDescription": "Escolha o agente que utiliza e copie a configuração de conexão correspondente.",
|
||||
"mcpNotRunningHint": "O serviço MCP não está ativado. Ative-o",
|
||||
"webApiConfigReadOnlyHelp": "A versão web não permite alterar as definições da API online. Atualize as opções de arranque do servidor ou o ficheiro de configuração e reinicie o serviço para aplicar as alterações.",
|
||||
"webMcpApiTokenHint": "Se configurou um token de API, é necessário fornecer as informações de autenticação ao ligar o agente",
|
||||
"useMcpApiTokenTemplate": "Mudar para a configuração autenticada",
|
||||
"mcpRequiresTcp": "As instruções de conexão de agentes estão disponíveis quando o protocolo da API é TCP."
|
||||
}
|
||||
@@ -432,5 +432,14 @@
|
||||
"booleanValueHint": "true / false",
|
||||
"androidForegroundServiceChannel": "Фоновая служба Gopeed",
|
||||
"androidForegroundNotificationTitle": "Gopeed работает",
|
||||
"androidForegroundNotificationText": "Загрузки могут продолжаться в фоновом режиме"
|
||||
"androidForegroundNotificationText": "Загрузки могут продолжаться в фоновом режиме",
|
||||
"mcpDescription": "Сотрудничайте с ИИ-агентами и открывайте безграничные возможности",
|
||||
"connectAgent": "Руководство по подключению",
|
||||
"connectMcpAgent": "Подключить MCP-агента",
|
||||
"mcpAgentDialogDescription": "Выберите используемого агента и скопируйте его конфигурацию подключения.",
|
||||
"mcpNotRunningHint": "Служба MCP не включена. Включите её",
|
||||
"webApiConfigReadOnlyHelp": "В веб-версии нельзя изменить настройки API онлайн. Измените параметры запуска сервера или файл конфигурации, затем перезапустите службу.",
|
||||
"webMcpApiTokenHint": "Если настроен API-токен, при подключении агента необходимо передать данные аутентификации",
|
||||
"useMcpApiTokenTemplate": "Переключиться на конфигурацию с аутентификацией",
|
||||
"mcpRequiresTcp": "Инструкции по подключению агентов доступны, когда протокол API — TCP."
|
||||
}
|
||||
@@ -432,5 +432,14 @@
|
||||
"booleanValueHint": "true / false",
|
||||
"androidForegroundServiceChannel": "Gopeed பின்னணி சேவை",
|
||||
"androidForegroundNotificationTitle": "Gopeed இயங்குகிறது",
|
||||
"androidForegroundNotificationText": "பதிவிறக்கங்கள் பின்னணியில் தொடரலாம்"
|
||||
"androidForegroundNotificationText": "பதிவிறக்கங்கள் பின்னணியில் தொடரலாம்",
|
||||
"mcpDescription": "AI முகவர்களுடன் இணைந்து எல்லையற்ற வாய்ப்புகளை ஆராயுங்கள்",
|
||||
"connectAgent": "இணைப்பு வழிகாட்டி",
|
||||
"connectMcpAgent": "MCP முகவரை இணை",
|
||||
"mcpAgentDialogDescription": "நீங்கள் பயன்படுத்தும் முகவரைத் தேர்ந்தெடுத்து அதன் இணைப்பு உள்ளமைவை நகலெடுக்கவும்.",
|
||||
"mcpNotRunningHint": "MCP சேவை இயக்கப்படவில்லை. அதை இயக்கவும்",
|
||||
"webApiConfigReadOnlyHelp": "இணையப் பதிப்பில் API அமைப்புகளை ஆன்லைனில் மாற்ற முடியாது. சேவையக தொடக்க விருப்பங்கள் அல்லது உள்ளமைவு கோப்பை மாற்றி, மாற்றங்கள் செயல்பட சேவையை மறுதொடக்கம் செய்யவும்.",
|
||||
"webMcpApiTokenHint": "API டோக்கனை அமைத்திருந்தால், முகவரை இணைக்கும்போது அங்கீகாரத் தகவலை அனுப்ப வேண்டும்",
|
||||
"useMcpApiTokenTemplate": "அங்கீகார உள்ளமைவுக்கு மாறு",
|
||||
"mcpRequiresTcp": "API நெறிமுறை TCP ஆக இருக்கும்போது முகவர் இணைப்பு வழிமுறைகள் கிடைக்கும்."
|
||||
}
|
||||
@@ -432,5 +432,14 @@
|
||||
"booleanValueHint": "true / false",
|
||||
"androidForegroundServiceChannel": "Gopeed arka plan hizmeti",
|
||||
"androidForegroundNotificationTitle": "Gopeed çalışıyor",
|
||||
"androidForegroundNotificationText": "İndirmeler arka planda devam edebilir"
|
||||
"androidForegroundNotificationText": "İndirmeler arka planda devam edebilir",
|
||||
"mcpDescription": "Yapay zekâ ajanlarıyla iş birliği yapın, sınırsız olanakları keşfedin",
|
||||
"connectAgent": "Bağlantı rehberi",
|
||||
"connectMcpAgent": "MCP ajanı bağla",
|
||||
"mcpAgentDialogDescription": "Kullandığınız ajanı seçin ve bağlantı yapılandırmasını kopyalayın.",
|
||||
"mcpNotRunningHint": "MCP hizmeti etkin değil. Lütfen etkinleştirin",
|
||||
"webApiConfigReadOnlyHelp": "Web sürümü API ayarlarının çevrimiçi değiştirilmesini desteklemez. Sunucu başlatma seçeneklerini veya yapılandırma dosyasını güncelleyin ve değişikliklerin etkili olması için hizmeti yeniden başlatın.",
|
||||
"webMcpApiTokenHint": "Bir API anahtarı yapılandırdıysanız, ajan bağlanırken kimlik doğrulama bilgileri iletilmelidir",
|
||||
"useMcpApiTokenTemplate": "Kimlik doğrulamalı yapılandırmaya geç",
|
||||
"mcpRequiresTcp": "Ajan bağlantı yönergeleri API protokolü TCP olduğunda kullanılabilir."
|
||||
}
|
||||
@@ -432,5 +432,14 @@
|
||||
"booleanValueHint": "true / false",
|
||||
"androidForegroundServiceChannel": "Фонова служба Gopeed",
|
||||
"androidForegroundNotificationTitle": "Gopeed працює",
|
||||
"androidForegroundNotificationText": "Завантаження можуть продовжуватися у фоновому режимі"
|
||||
"androidForegroundNotificationText": "Завантаження можуть продовжуватися у фоновому режимі",
|
||||
"mcpDescription": "Співпрацюйте з ШІ-агентами та відкривайте безмежні можливості",
|
||||
"connectAgent": "Посібник із підключення",
|
||||
"connectMcpAgent": "Підключити MCP-агента",
|
||||
"mcpAgentDialogDescription": "Виберіть агента, якого використовуєте, і скопіюйте його конфігурацію підключення.",
|
||||
"mcpNotRunningHint": "Службу MCP не ввімкнено. Увімкніть її",
|
||||
"webApiConfigReadOnlyHelp": "У вебверсії не можна змінювати налаштування API онлайн. Змініть параметри запуску сервера або файл конфігурації, а потім перезапустіть службу.",
|
||||
"webMcpApiTokenHint": "Якщо налаштовано API-токен, під час підключення агента потрібно передати дані автентифікації",
|
||||
"useMcpApiTokenTemplate": "Перейти до конфігурації з автентифікацією",
|
||||
"mcpRequiresTcp": "Інструкції з підключення агентів доступні, коли протокол API — TCP."
|
||||
}
|
||||
@@ -432,5 +432,14 @@
|
||||
"booleanValueHint": "true / false",
|
||||
"androidForegroundServiceChannel": "Dịch vụ nền Gopeed",
|
||||
"androidForegroundNotificationTitle": "Gopeed đang chạy",
|
||||
"androidForegroundNotificationText": "Các lượt tải xuống có thể tiếp tục trong nền"
|
||||
"androidForegroundNotificationText": "Các lượt tải xuống có thể tiếp tục trong nền",
|
||||
"mcpDescription": "Cộng tác cùng AI Agent và khám phá khả năng vô hạn",
|
||||
"connectAgent": "Hướng dẫn kết nối",
|
||||
"connectMcpAgent": "Kết nối tác nhân MCP",
|
||||
"mcpAgentDialogDescription": "Chọn tác nhân bạn sử dụng rồi sao chép cấu hình kết nối tương ứng.",
|
||||
"mcpNotRunningHint": "Dịch vụ MCP chưa được bật. Hãy bật dịch vụ",
|
||||
"webApiConfigReadOnlyHelp": "Phiên bản web không hỗ trợ sửa cấu hình API trực tuyến. Hãy cập nhật tùy chọn khởi động máy chủ hoặc tệp cấu hình rồi khởi động lại dịch vụ để áp dụng thay đổi.",
|
||||
"webMcpApiTokenHint": "Nếu đã cấu hình API Token, bạn cần truyền thông tin xác thực khi kết nối tác nhân",
|
||||
"useMcpApiTokenTemplate": "Chuyển sang cấu hình có xác thực",
|
||||
"mcpRequiresTcp": "Hướng dẫn kết nối tác nhân khả dụng khi giao thức API là TCP."
|
||||
}
|
||||
@@ -432,5 +432,14 @@
|
||||
"booleanValueHint": "true / false",
|
||||
"androidForegroundServiceChannel": "Gopeed 后台服务",
|
||||
"androidForegroundNotificationTitle": "Gopeed 正在运行",
|
||||
"androidForegroundNotificationText": "下载可在后台继续"
|
||||
"androidForegroundNotificationText": "下载可在后台继续",
|
||||
"mcpDescription": "与 AI Agent 的协同,探索无限可能",
|
||||
"connectAgent": "接入指南",
|
||||
"connectMcpAgent": "接入 MCP Agent",
|
||||
"mcpAgentDialogDescription": "选择你使用的 Agent,然后复制对应的接入配置。",
|
||||
"mcpNotRunningHint": "尚未启用 MCP 服务,请先开启",
|
||||
"webMcpApiTokenHint": "若已配置 API Token,接入 Agent 时需要传递认证信息",
|
||||
"useMcpApiTokenTemplate": "切换为认证配置",
|
||||
"webApiConfigReadOnlyHelp": "Web 版本不支持在线修改 API 配置。请修改服务端启动参数或配置文件,并重启服务后使配置生效。",
|
||||
"mcpRequiresTcp": "API 通讯协议为 TCP 时才能使用 Agent 接入指引。"
|
||||
}
|
||||
@@ -432,5 +432,14 @@
|
||||
"booleanValueHint": "true / false",
|
||||
"androidForegroundServiceChannel": "Gopeed 背景服務",
|
||||
"androidForegroundNotificationTitle": "Gopeed 正在執行",
|
||||
"androidForegroundNotificationText": "下載可在背景繼續"
|
||||
"androidForegroundNotificationText": "下載可在背景繼續",
|
||||
"mcpDescription": "與 AI Agent 的協同,探索無限可能",
|
||||
"connectAgent": "連接指南",
|
||||
"connectMcpAgent": "接入 MCP Agent",
|
||||
"mcpAgentDialogDescription": "選擇你使用的 Agent,然後複製對應的接入設定。",
|
||||
"mcpNotRunningHint": "尚未啟用 MCP 服務,請先開啟",
|
||||
"webApiConfigReadOnlyHelp": "Web 版本不支援線上修改 API 設定。請修改伺服器啟動參數或設定檔,並重新啟動服務後使設定生效。",
|
||||
"webMcpApiTokenHint": "若已設定 API Token,接入 Agent 時需要傳遞認證資訊",
|
||||
"useMcpApiTokenTemplate": "切換為認證設定",
|
||||
"mcpRequiresTcp": "API 通訊協定為 TCP 時才能使用 Agent 接入指引。"
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import 'package:flutter/material.dart' show Icons;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
|
||||
|
||||
import '../../l10n/l10n.dart';
|
||||
import 'app_tooltip.dart';
|
||||
|
||||
class AppCopyIconButton extends StatefulWidget {
|
||||
const AppCopyIconButton({
|
||||
super.key,
|
||||
required this.text,
|
||||
this.enabled = true,
|
||||
this.dimension = 28,
|
||||
this.iconSize = 16,
|
||||
});
|
||||
|
||||
final String text;
|
||||
final bool enabled;
|
||||
final double dimension;
|
||||
final double iconSize;
|
||||
|
||||
@override
|
||||
State<AppCopyIconButton> createState() => _AppCopyIconButtonState();
|
||||
}
|
||||
|
||||
class _AppCopyIconButtonState extends State<AppCopyIconButton> {
|
||||
bool _copied = false;
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant AppCopyIconButton oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.text != oldWidget.text || (!widget.enabled && oldWidget.enabled)) {
|
||||
_copied = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppTooltip(
|
||||
message: _copied ? context.l10n.copied : context.l10n.copy,
|
||||
child: SizedBox.square(
|
||||
dimension: widget.dimension,
|
||||
child: shad.IconButton.ghost(
|
||||
size: shad.ButtonSize.xSmall,
|
||||
onPressed: widget.enabled ? _copy : null,
|
||||
icon: Icon(_copied ? Icons.check : Icons.copy_outlined, size: widget.iconSize),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _copy() async {
|
||||
await Clipboard.setData(ClipboardData(text: widget.text));
|
||||
if (mounted) {
|
||||
setState(() => _copied = true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,20 +31,28 @@ class AppLoadingButton extends StatelessWidget {
|
||||
final replaceChild = loading && icon == null;
|
||||
final leading = loading && icon != null ? _LoadingIcon(variant: variant) : icon;
|
||||
final effectiveChild = replaceChild ? _LoadingIcon(variant: variant) : child;
|
||||
final content = leading == null
|
||||
? effectiveChild
|
||||
: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
leading,
|
||||
const SizedBox(width: AppDesignTokens.space8),
|
||||
effectiveChild,
|
||||
],
|
||||
);
|
||||
final effectiveOnPressed = loading ? null : onPressed;
|
||||
|
||||
return switch (variant) {
|
||||
AppLoadingButtonVariant.secondary => shad.SecondaryButton(
|
||||
onPressed: effectiveOnPressed,
|
||||
leading: leading,
|
||||
alignment: alignment,
|
||||
child: effectiveChild,
|
||||
child: content,
|
||||
),
|
||||
AppLoadingButtonVariant.primary => AppPrimaryButton(
|
||||
onPressed: effectiveOnPressed,
|
||||
leading: leading,
|
||||
alignment: alignment,
|
||||
child: effectiveChild,
|
||||
child: content,
|
||||
),
|
||||
AppLoadingButtonVariant.brand => shad.ButtonStyleOverride(
|
||||
decoration: (context, states, value) {
|
||||
@@ -70,12 +78,7 @@ class AppLoadingButton extends StatelessWidget {
|
||||
? palette.brandForeground.withValues(alpha: 0.55)
|
||||
: palette.brandForeground,
|
||||
),
|
||||
child: shad.SecondaryButton(
|
||||
onPressed: effectiveOnPressed,
|
||||
leading: leading,
|
||||
alignment: alignment,
|
||||
child: effectiveChild,
|
||||
),
|
||||
child: shad.SecondaryButton(onPressed: effectiveOnPressed, alignment: alignment, child: content),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -31,6 +31,16 @@ class AppPrimaryButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final palette = AppPalette.of(context);
|
||||
final content = leading == null && trailing == null
|
||||
? child
|
||||
: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (leading != null) ...[leading!, const SizedBox(width: AppDesignTokens.space8)],
|
||||
child,
|
||||
if (trailing != null) ...[const SizedBox(width: AppDesignTokens.space8), trailing!],
|
||||
],
|
||||
);
|
||||
|
||||
return shad.ButtonStyleOverride(
|
||||
decoration: (context, states, value) {
|
||||
@@ -48,14 +58,12 @@ class AppPrimaryButton extends StatelessWidget {
|
||||
},
|
||||
child: shad.PrimaryButton(
|
||||
onPressed: onPressed,
|
||||
leading: leading,
|
||||
trailing: trailing,
|
||||
alignment: alignment,
|
||||
size: size,
|
||||
density: density,
|
||||
shape: shape,
|
||||
disableTransition: disableTransition,
|
||||
child: child,
|
||||
child: content,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user