feature: added ability to change probe endpoint for offline check + edit settings in offline mode (#177)
This commit is contained in:
@@ -427,6 +427,7 @@ class ApiService {
|
||||
/// - `queryParams`: Optional query parameters
|
||||
/// - `followRedirects`: If false, will throw a [RedirectException] on 301/302 status codes.
|
||||
Future<bool> isReachable({
|
||||
String endpoint = '/',
|
||||
Duration timeout = const Duration(seconds: 8),
|
||||
}) async {
|
||||
if (_baseUrl == null || _baseUrl!.isEmpty) return false;
|
||||
@@ -437,7 +438,7 @@ class ApiService {
|
||||
httpClient.badCertificateCallback = (cert, host, port) => true;
|
||||
}
|
||||
try {
|
||||
final uri = _buildUri(endpoint: '/');
|
||||
final uri = _buildUri(endpoint: endpoint);
|
||||
final headers = getAuthHeaders(authMethod: AuthMethod.auto);
|
||||
if (_userAgent != null) headers['User-Agent'] = _userAgent!;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:calibre_web_companion/core/services/api_service.dart';
|
||||
|
||||
@@ -30,9 +31,26 @@ class ConnectivityService {
|
||||
final baseUrl = apiService.getBaseUrl();
|
||||
if (baseUrl.isEmpty) return false;
|
||||
|
||||
return await apiService.isReachable();
|
||||
return await apiService.isReachable(endpoint: await _probeEndpoint());
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _probeEndpoint() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final override = prefs.getString('reachability_probe_endpoint')?.trim();
|
||||
if (override != null && override.isNotEmpty) return override;
|
||||
|
||||
switch (prefs.getString('server_type')) {
|
||||
case 'calibre':
|
||||
return '/ajax/library-info';
|
||||
case 'grimmory':
|
||||
return '/catalog';
|
||||
case 'opds':
|
||||
return '';
|
||||
default:
|
||||
return '/ajax/listbooks?limit=1';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import 'package:calibre_web_companion/features/download_service/presentation/pag
|
||||
import 'package:calibre_web_companion/features/settings/bloc/settings_bloc.dart';
|
||||
import 'package:calibre_web_companion/features/settings/bloc/settings_state.dart';
|
||||
import 'package:calibre_web_companion/features/offline/cubit/connectivity_cubit.dart';
|
||||
import 'package:calibre_web_companion/features/offline/presentation/pages/offline_library_page.dart';
|
||||
import 'package:calibre_web_companion/features/offline/presentation/pages/offline_home_page.dart';
|
||||
|
||||
class HomePage extends StatelessWidget {
|
||||
const HomePage({super.key});
|
||||
@@ -27,57 +27,13 @@ class HomePage extends StatelessWidget {
|
||||
return BlocBuilder<ConnectivityCubit, ConnectivityStatus>(
|
||||
builder: (context, connectivity) {
|
||||
if (connectivity == ConnectivityStatus.offline) {
|
||||
return _buildOffline(context, localizations);
|
||||
return const OfflineHomePage();
|
||||
}
|
||||
return _buildOnline(context, localizations);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOffline(BuildContext context, AppLocalizations localizations) {
|
||||
final theme = Theme.of(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(localizations.offline),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh_rounded),
|
||||
tooltip: localizations.retry,
|
||||
onPressed: () => context.read<ConnectivityCubit>().recheck(),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
color: theme.colorScheme.secondaryContainer,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.cloud_off_rounded,
|
||||
size: 20,
|
||||
color: theme.colorScheme.onSecondaryContainer,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
localizations.offlineBannerMessage,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Expanded(child: OfflineLibraryPage()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOnline(BuildContext context, AppLocalizations localizations) {
|
||||
return BlocBuilder<HomePageBloc, HomePageState>(
|
||||
builder: (context, homeState) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:calibre_web_companion/features/homepage/presentation/pages/home_
|
||||
import 'package:calibre_web_companion/core/services/snackbar.dart';
|
||||
import 'package:calibre_web_companion/l10n/app_localizations.dart';
|
||||
import 'package:calibre_web_companion/features/login/presentation/widgets/login_form_widget.dart';
|
||||
import 'package:calibre_web_companion/features/offline/cubit/connectivity_cubit.dart';
|
||||
|
||||
class LoginPage extends StatelessWidget {
|
||||
const LoginPage({super.key});
|
||||
@@ -23,6 +24,7 @@ class LoginPage extends StatelessWidget {
|
||||
body: BlocListener<LoginBloc, LoginState>(
|
||||
listener: (context, state) {
|
||||
if (state.status == LoginStatus.success) {
|
||||
context.read<ConnectivityCubit>().reportSuccess();
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (context) => HomePage()),
|
||||
);
|
||||
|
||||
@@ -21,6 +21,7 @@ class LoginSettingsBloc extends Bloc<LoginSettingsEvent, LoginSettingsState> {
|
||||
on<UpdateCustomHeaderKey>(_onUpdateCustomHeaderKey);
|
||||
on<UpdateCustomHeaderValue>(_onUpdateCustomHeaderValue);
|
||||
on<UpdateBasePath>(_onUpdateBasePath);
|
||||
on<UpdateReachabilityProbe>(_onUpdateReachabilityProbe);
|
||||
on<UpdateAllowSelfSigned>(_onUpdateAllowSelfSigned);
|
||||
}
|
||||
|
||||
@@ -34,6 +35,8 @@ class LoginSettingsBloc extends Bloc<LoginSettingsEvent, LoginSettingsState> {
|
||||
final headers = await loginSettingsRepository.getCustomHeaders();
|
||||
final basePath =
|
||||
await loginSettingsRepository.getBasePath(); // Base Path laden
|
||||
final reachabilityProbe =
|
||||
await loginSettingsRepository.getReachabilityProbe();
|
||||
final allowSelfSigned =
|
||||
await loginSettingsRepository.getAllowSelfSigned();
|
||||
|
||||
@@ -41,6 +44,7 @@ class LoginSettingsBloc extends Bloc<LoginSettingsEvent, LoginSettingsState> {
|
||||
state.copyWith(
|
||||
customHeaders: headers,
|
||||
basePath: basePath,
|
||||
reachabilityProbe: reachabilityProbe,
|
||||
allowSelfSigned: allowSelfSigned,
|
||||
isLoading: false,
|
||||
),
|
||||
@@ -141,6 +145,18 @@ class LoginSettingsBloc extends Bloc<LoginSettingsEvent, LoginSettingsState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onUpdateReachabilityProbe(
|
||||
UpdateReachabilityProbe event,
|
||||
Emitter<LoginSettingsState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(reachabilityProbe: event.endpoint));
|
||||
try {
|
||||
await loginSettingsRepository.saveReachabilityProbe(event.endpoint.trim());
|
||||
} catch (e) {
|
||||
_logger.e('Error saving reachability probe endpoint: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onUpdateAllowSelfSigned(
|
||||
UpdateAllowSelfSigned event,
|
||||
Emitter<LoginSettingsState> emit,
|
||||
|
||||
@@ -53,6 +53,15 @@ class UpdateBasePath extends LoginSettingsEvent {
|
||||
List<Object?> get props => [basePath];
|
||||
}
|
||||
|
||||
class UpdateReachabilityProbe extends LoginSettingsEvent {
|
||||
final String endpoint;
|
||||
|
||||
const UpdateReachabilityProbe(this.endpoint);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [endpoint];
|
||||
}
|
||||
|
||||
class UpdateAllowSelfSigned extends LoginSettingsEvent {
|
||||
final bool allowSelfSigned;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:calibre_web_companion/features/login_settings/data/models/custom
|
||||
class LoginSettingsState extends Equatable {
|
||||
final List<CustomHeaderModel> customHeaders;
|
||||
final String basePath;
|
||||
final String reachabilityProbe;
|
||||
final bool isLoading;
|
||||
final bool isSaved;
|
||||
final bool allowSelfSigned;
|
||||
@@ -13,6 +14,7 @@ class LoginSettingsState extends Equatable {
|
||||
const LoginSettingsState({
|
||||
this.customHeaders = const [],
|
||||
this.basePath = '',
|
||||
this.reachabilityProbe = '',
|
||||
this.isLoading = false,
|
||||
this.isSaved = false,
|
||||
this.allowSelfSigned = false,
|
||||
@@ -22,6 +24,7 @@ class LoginSettingsState extends Equatable {
|
||||
LoginSettingsState copyWith({
|
||||
List<CustomHeaderModel>? customHeaders,
|
||||
String? basePath,
|
||||
String? reachabilityProbe,
|
||||
bool? isLoading,
|
||||
bool? isSaved,
|
||||
bool? allowSelfSigned,
|
||||
@@ -30,6 +33,7 @@ class LoginSettingsState extends Equatable {
|
||||
return LoginSettingsState(
|
||||
customHeaders: customHeaders ?? this.customHeaders,
|
||||
basePath: basePath ?? this.basePath,
|
||||
reachabilityProbe: reachabilityProbe ?? this.reachabilityProbe,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
isSaved: isSaved ?? this.isSaved,
|
||||
allowSelfSigned: allowSelfSigned ?? this.allowSelfSigned,
|
||||
@@ -41,6 +45,7 @@ class LoginSettingsState extends Equatable {
|
||||
List<Object?> get props => [
|
||||
customHeaders,
|
||||
basePath,
|
||||
reachabilityProbe,
|
||||
isLoading,
|
||||
isSaved,
|
||||
allowSelfSigned,
|
||||
|
||||
@@ -18,6 +18,7 @@ class LoginSettingsLocalDataSource {
|
||||
|
||||
static const String _customHeadersKey = 'custom_login_headers';
|
||||
static const String _basePathKey = 'base_path';
|
||||
static const String _reachabilityProbeKey = 'reachability_probe_endpoint';
|
||||
|
||||
Future<List<CustomHeaderModel>> getCustomHeaders() async {
|
||||
try {
|
||||
@@ -75,6 +76,30 @@ class LoginSettingsLocalDataSource {
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> getReachabilityProbe() async {
|
||||
try {
|
||||
return preferences.getString(_reachabilityProbeKey) ?? '';
|
||||
} catch (e) {
|
||||
logger.e('Error loading reachability probe endpoint: $e');
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveReachabilityProbe(String endpoint) async {
|
||||
try {
|
||||
final trimmed = endpoint.trim();
|
||||
if (trimmed.isEmpty) {
|
||||
await preferences.remove(_reachabilityProbeKey);
|
||||
} else {
|
||||
await preferences.setString(_reachabilityProbeKey, trimmed);
|
||||
}
|
||||
logger.i('Saved reachability probe endpoint: $trimmed');
|
||||
} catch (e) {
|
||||
logger.e('Error saving reachability probe endpoint: $e');
|
||||
throw Exception('Failed to save reachability probe endpoint: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> getAllowSelfSigned() async {
|
||||
try {
|
||||
final bool allowSelfSigned =
|
||||
|
||||
@@ -53,6 +53,22 @@ class LoginSettingsRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> getReachabilityProbe() async {
|
||||
try {
|
||||
return await loginSettingsLocalDataSource.getReachabilityProbe();
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveReachabilityProbe(String endpoint) async {
|
||||
try {
|
||||
await loginSettingsLocalDataSource.saveReachabilityProbe(endpoint);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> getAllowSelfSigned() async {
|
||||
try {
|
||||
return await loginSettingsLocalDataSource.getAllowSelfSigned();
|
||||
|
||||
@@ -119,6 +119,16 @@ class _LoginSettingsPage extends State<LoginSettingsPage> {
|
||||
),
|
||||
_buildBasePathSection(context, state, localizations),
|
||||
|
||||
_buildSectionTitle(
|
||||
context,
|
||||
localizations.reachabilityCheckTitle,
|
||||
),
|
||||
_buildReachabilityProbeSection(
|
||||
context,
|
||||
state,
|
||||
localizations,
|
||||
),
|
||||
|
||||
_buildSectionTitle(context, localizations.sslSettings),
|
||||
_buildSSLSettingsSection(context, localizations, state),
|
||||
|
||||
@@ -266,6 +276,77 @@ class _LoginSettingsPage extends State<LoginSettingsPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildReachabilityProbeSection(
|
||||
BuildContext context,
|
||||
LoginSettingsState state,
|
||||
AppLocalizations localizations,
|
||||
) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
elevation: 3,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.network_check_rounded,
|
||||
size: 28,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
localizations.reachabilityCheckTitle,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
localizations.reachabilityCheckDescription,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
TextFormField(
|
||||
initialValue: state.reachabilityProbe,
|
||||
onChanged: (value) {
|
||||
context.read<LoginSettingsBloc>().add(
|
||||
UpdateReachabilityProbe(value),
|
||||
);
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
labelText: localizations.reachabilityCheckLabel,
|
||||
hintText: localizations.reachabilityCheckHint,
|
||||
prefixIcon: const Icon(Icons.wifi_tethering_rounded),
|
||||
filled: true,
|
||||
fillColor: Theme.of(context).colorScheme.surface,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 14.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBasePathSection(
|
||||
BuildContext context,
|
||||
LoginSettingsState state,
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'package:calibre_web_companion/l10n/app_localizations.dart';
|
||||
import 'package:calibre_web_companion/features/offline/cubit/connectivity_cubit.dart';
|
||||
import 'package:calibre_web_companion/features/offline/presentation/pages/offline_library_page.dart';
|
||||
import 'package:calibre_web_companion/features/login_settings/presentation/pages/login_settings_page.dart';
|
||||
import 'package:calibre_web_companion/features/login_settings/presentation/pages/connection_diagnostics_page.dart';
|
||||
import 'package:calibre_web_companion/features/login/presentation/pages/login_page.dart';
|
||||
|
||||
class OfflineHomePage extends StatefulWidget {
|
||||
const OfflineHomePage({super.key});
|
||||
|
||||
@override
|
||||
State<OfflineHomePage> createState() => _OfflineHomePageState();
|
||||
}
|
||||
|
||||
class _OfflineHomePageState extends State<OfflineHomePage> {
|
||||
int _index = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localizations = AppLocalizations.of(context)!;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(localizations.offline),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh_rounded),
|
||||
tooltip: localizations.retry,
|
||||
onPressed: () => context.read<ConnectivityCubit>().recheck(),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: IndexedStack(
|
||||
index: _index,
|
||||
children: [
|
||||
_buildLibraryTab(context, localizations),
|
||||
const _OfflineMenu(),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: NavigationBar(
|
||||
selectedIndex: _index,
|
||||
labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
|
||||
onDestinationSelected: (i) => setState(() => _index = i),
|
||||
destinations: [
|
||||
NavigationDestination(
|
||||
icon: const Icon(Icons.book_rounded),
|
||||
label: localizations.books,
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: const Icon(Icons.settings_rounded),
|
||||
label: localizations.settings,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLibraryTab(
|
||||
BuildContext context,
|
||||
AppLocalizations localizations,
|
||||
) {
|
||||
final theme = Theme.of(context);
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
color: theme.colorScheme.secondaryContainer,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.cloud_off_rounded,
|
||||
size: 20,
|
||||
color: theme.colorScheme.onSecondaryContainer,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
localizations.offlineBannerMessage,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Expanded(child: OfflineLibraryPage()),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OfflineMenu extends StatelessWidget {
|
||||
const _OfflineMenu();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localizations = AppLocalizations.of(context)!;
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
children: [
|
||||
_MenuCard(
|
||||
icon: Icons.refresh_rounded,
|
||||
title: localizations.tryAgain,
|
||||
onTap: () => context.read<ConnectivityCubit>().recheck(),
|
||||
),
|
||||
_MenuCard(
|
||||
icon: Icons.link_rounded,
|
||||
title: localizations.connectionSettings,
|
||||
onTap:
|
||||
() => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const LoginSettingsPage()),
|
||||
),
|
||||
),
|
||||
_MenuCard(
|
||||
icon: Icons.troubleshoot_rounded,
|
||||
title: localizations.connectionDiagnostics,
|
||||
subtitle: localizations.connectionDiagnosticsSubtitle,
|
||||
onTap:
|
||||
() => Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const ConnectionDiagnosticsPage(),
|
||||
),
|
||||
),
|
||||
),
|
||||
_MenuCard(
|
||||
icon: Icons.manage_accounts_rounded,
|
||||
title: localizations.accounts,
|
||||
onTap:
|
||||
() => Navigator.of(
|
||||
context,
|
||||
).push(MaterialPageRoute(builder: (_) => const LoginPage())),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MenuCard extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _MenuCard({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.onTap,
|
||||
this.subtitle,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||
elevation: 3,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 28, color: theme.colorScheme.secondary),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: theme.textTheme.titleMedium),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.chevron_right_rounded,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -325,6 +325,10 @@
|
||||
"basePathLabel": "Base Path",
|
||||
"basePathHint": "e.g., /opds or /calibre",
|
||||
"basePathDescription": "Define a custom base path for API requests",
|
||||
"reachabilityCheckTitle": "Reachability Check",
|
||||
"reachabilityCheckLabel": "Probe endpoint",
|
||||
"reachabilityCheckHint": "e.g., /ajax/listbooks?limit=1",
|
||||
"reachabilityCheckDescription": "The endpoint used to test whether the server is online. Leave empty to use the default for your server type.",
|
||||
"bookCover": "Book cover",
|
||||
"currentCover": "Current cover",
|
||||
"newCover": "New cover",
|
||||
|
||||
Reference in New Issue
Block a user