mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 12:38:24 +00:00
Add "Always download from CDN" option to hide IP from GitHub
Global setting so users can always download from cdn.zapstore.dev instead of the original URL (e.g. GitHub) to avoid exposing their IP to the host. - SecureStorageService: getAlwaysUseCdn() / setAlwaysUseCdn() and alwaysUseCdnProvider - PackageManager: _resolveDownloadUrl() reads the setting and returns CDN or original URL - InstallAlertDialog: switch "Download from CDN (hides IP)"; relay-signed apps show dynamic text (CDN vs original URL) based on switch state - InstallButton: saves alwaysUseCdn from dialog result when user confirms - ProfileScreen: "Always download from CDN" toggle in Settings Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
288cb8ef16
commit
b28ebd5a39
@@ -21,6 +21,7 @@ import 'package:zapstore/widgets/common/profile_identity_row.dart';
|
||||
import 'package:zapstore/widgets/app_card.dart';
|
||||
import 'package:zapstore/theme.dart';
|
||||
import 'package:zapstore/services/notification_service.dart';
|
||||
import 'package:zapstore/services/secure_storage_service.dart';
|
||||
import 'package:zapstore/widgets/common/note_parser.dart';
|
||||
import 'package:zapstore/widgets/nwc_widgets.dart';
|
||||
import 'package:zapstore/widgets/relay_management_card.dart';
|
||||
@@ -67,6 +68,11 @@ class ProfileScreen extends ConsumerWidget {
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// CDN Download Setting
|
||||
const _CdnDownloadCard(),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Data Management Section
|
||||
const _DataManagementSection(),
|
||||
|
||||
@@ -185,17 +191,11 @@ class _AuthenticationSection extends ConsumerWidget {
|
||||
FilledButton.icon(
|
||||
onPressed: () => _signOut(context, ref),
|
||||
icon: const Icon(Icons.logout, size: 14),
|
||||
label: const Text(
|
||||
'Sign Out',
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
label: const Text('Sign Out', style: TextStyle(fontSize: 12)),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Colors.red.shade900,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
minimumSize: const Size(0, 0),
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
@@ -1334,6 +1334,29 @@ class _EmptyState extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _CdnDownloadCard extends ConsumerWidget {
|
||||
const _CdnDownloadCard();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final alwaysUseCdnAsync = ref.watch(alwaysUseCdnProvider);
|
||||
final alwaysUseCdn = alwaysUseCdnAsync.valueOrNull ?? false;
|
||||
final secureStorage = ref.read(secureStorageServiceProvider);
|
||||
|
||||
return Card(
|
||||
child: SwitchListTile(
|
||||
title: const Text('Always download from CDN'),
|
||||
subtitle: const Text('Hides your IP from GitHub/Microsoft'),
|
||||
value: alwaysUseCdn,
|
||||
onChanged: (value) async {
|
||||
await secureStorage.setAlwaysUseCdn(value);
|
||||
ref.invalidate(alwaysUseCdnProvider);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DataManagementSection extends ConsumerWidget {
|
||||
const _DataManagementSection();
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:models/models.dart';
|
||||
import 'package:zapstore/services/package_manager/device_capabilities.dart';
|
||||
import 'package:zapstore/services/package_manager/dummy_package_manager.dart';
|
||||
import 'package:zapstore/services/package_manager/install_operation.dart';
|
||||
import 'package:zapstore/services/secure_storage_service.dart';
|
||||
export 'device_capabilities.dart';
|
||||
export 'install_operation.dart';
|
||||
|
||||
@@ -355,8 +356,8 @@ abstract class PackageManager extends StateNotifier<PackageManagerState> {
|
||||
}
|
||||
}
|
||||
|
||||
final downloadUrl = target.urls.firstOrNull;
|
||||
if (downloadUrl == null || downloadUrl.isEmpty) {
|
||||
final resolved = await _resolveDownloadUrl(target);
|
||||
if (resolved == null) {
|
||||
setOperation(
|
||||
appId,
|
||||
OperationFailed(
|
||||
@@ -394,9 +395,9 @@ abstract class PackageManager extends StateNotifier<PackageManagerState> {
|
||||
// Queue items with staggered delays to prevent UI flood
|
||||
for (var i = 0; i < toQueue.length; i++) {
|
||||
final item = toQueue[i];
|
||||
final downloadUrl = item.target.urls.firstOrNull;
|
||||
final resolved = await _resolveDownloadUrl(item.target);
|
||||
|
||||
if (downloadUrl == null || downloadUrl.isEmpty) {
|
||||
if (resolved == null) {
|
||||
setOperation(
|
||||
item.appId,
|
||||
OperationFailed(
|
||||
@@ -709,6 +710,20 @@ abstract class PackageManager extends StateNotifier<PackageManagerState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve download URL: CDN if setting enabled, else original URL.
|
||||
Future<({String url, bool isCdn})?> _resolveDownloadUrl(FileMetadata target) async {
|
||||
final secureStorage = ref.read(secureStorageServiceProvider);
|
||||
final alwaysUseCdn = await secureStorage.getAlwaysUseCdn();
|
||||
if (alwaysUseCdn) {
|
||||
final hash = target.hash;
|
||||
if (hash.isEmpty) return null;
|
||||
return (url: 'https://cdn.zapstore.dev/$hash', isCdn: true);
|
||||
}
|
||||
final url = target.urls.firstOrNull;
|
||||
if (url == null || url.isEmpty) return null;
|
||||
return (url: url, isCdn: false);
|
||||
}
|
||||
|
||||
/// Re-download from CDN after a hash mismatch on the original source.
|
||||
@protected
|
||||
Future<void> retryDownloadFromCdn(String appId, FileMetadata target) {
|
||||
@@ -994,8 +1009,8 @@ abstract class PackageManager extends StateNotifier<PackageManagerState> {
|
||||
continue;
|
||||
}
|
||||
|
||||
final downloadUrl = op.target.urls.firstOrNull;
|
||||
if (downloadUrl == null || downloadUrl.isEmpty) {
|
||||
final resolved = await _resolveDownloadUrl(op.target);
|
||||
if (resolved == null) {
|
||||
setOperation(
|
||||
appId,
|
||||
OperationFailed(
|
||||
@@ -1011,9 +1026,9 @@ abstract class PackageManager extends StateNotifier<PackageManagerState> {
|
||||
await _startDownloadTask(
|
||||
appId,
|
||||
op.target,
|
||||
downloadUrl,
|
||||
resolved.url,
|
||||
displayName: op.displayName,
|
||||
isCdnRetry: false,
|
||||
isCdnRetry: resolved.isCdn,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +126,21 @@ class SecureStorageService {
|
||||
value: jsonEncode(relays.toList()),
|
||||
);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Always Use CDN (privacy: hide IP from GitHub)
|
||||
// =========================================================================
|
||||
|
||||
static const _alwaysUseCdnKey = 'always_use_cdn';
|
||||
|
||||
Future<bool> getAlwaysUseCdn() async {
|
||||
final value = await _storage.read(key: _alwaysUseCdnKey);
|
||||
return value == 'true';
|
||||
}
|
||||
|
||||
Future<void> setAlwaysUseCdn(bool value) async {
|
||||
await _storage.write(key: _alwaysUseCdnKey, value: value.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/// Persists the AmberSigner pubkey in flutter_secure_storage.
|
||||
@@ -161,3 +176,11 @@ final hasNwcStringProvider = FutureProvider.autoDispose<bool>((ref) async {
|
||||
final secureStorage = ref.watch(secureStorageServiceProvider);
|
||||
return secureStorage.hasNWCString();
|
||||
});
|
||||
|
||||
/// Whether to always download from CDN instead of original URL.
|
||||
///
|
||||
/// Use `ref.invalidate(alwaysUseCdnProvider)` after updating to refresh UI.
|
||||
final alwaysUseCdnProvider = FutureProvider.autoDispose<bool>((ref) async {
|
||||
final secureStorage = ref.watch(secureStorageServiceProvider);
|
||||
return secureStorage.getAlwaysUseCdn();
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:zapstore/utils/extensions.dart';
|
||||
import 'package:zapstore/widgets/author_container.dart';
|
||||
import 'package:zapstore/widgets/common/base_dialog.dart';
|
||||
import 'package:zapstore/widgets/download_text_container.dart';
|
||||
import 'package:zapstore/services/secure_storage_service.dart';
|
||||
import 'package:zapstore/widgets/relevant_who_follow_container.dart';
|
||||
import 'package:zapstore/widgets/sign_in_button.dart';
|
||||
|
||||
@@ -32,9 +33,35 @@ class InstallAlertDialog extends HookConsumerWidget {
|
||||
_ => null,
|
||||
};
|
||||
if (publisher == null) {
|
||||
return const SizedBox.shrink();
|
||||
return BaseDialog(
|
||||
title: const BaseDialogTitle('Trust this app?'),
|
||||
content: const BaseDialogContent(
|
||||
children: [
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(24),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: null, // Disabled while loading
|
||||
child: const Text('Trust and install app'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
final trustedSignerNotifier = useState(false);
|
||||
final alwaysUseCdnAsync = ref.watch(alwaysUseCdnProvider);
|
||||
final alwaysUseCdnLocal = useState<bool?>(null);
|
||||
final alwaysUseCdn =
|
||||
alwaysUseCdnLocal.value ?? alwaysUseCdnAsync.valueOrNull ?? false;
|
||||
final signedInPubkey = ref.watch(Signer.activePubkeyProvider);
|
||||
final activeSigner = ref.watch(Signer.activeSignerProvider);
|
||||
final canPersistTrust = signedInPubkey != null && activeSigner != null;
|
||||
@@ -55,7 +82,14 @@ class InstallAlertDialog extends HookConsumerWidget {
|
||||
size: baseTextSize,
|
||||
),
|
||||
Gap(10),
|
||||
DownloadTextContainer(
|
||||
alwaysUseCdn
|
||||
? Text(
|
||||
'${app.name ?? app.identifier} will be installed from cdn.zapstore.dev.',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
)
|
||||
: DownloadTextContainer(
|
||||
beforeText:
|
||||
'${app.name ?? app.identifier} will be installed from its original release location:',
|
||||
oneLine: false,
|
||||
@@ -121,6 +155,27 @@ class InstallAlertDialog extends HookConsumerWidget {
|
||||
),
|
||||
],
|
||||
],
|
||||
const Gap(14),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Switch(
|
||||
value: alwaysUseCdn,
|
||||
onChanged: (value) {
|
||||
alwaysUseCdnLocal.value = value;
|
||||
},
|
||||
),
|
||||
Gap(4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Download from CDN (hides IP)',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
@@ -133,9 +188,10 @@ class InstallAlertDialog extends HookConsumerWidget {
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(
|
||||
context,
|
||||
).pop((trustPermanently: trustedSignerNotifier.value));
|
||||
Navigator.of(context).pop((
|
||||
trustPermanently: trustedSignerNotifier.value,
|
||||
alwaysUseCdn: alwaysUseCdn,
|
||||
));
|
||||
},
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:models/models.dart';
|
||||
import 'package:zapstore/services/notification_service.dart';
|
||||
import 'package:zapstore/services/package_manager/package_manager.dart';
|
||||
import 'package:zapstore/services/secure_storage_service.dart';
|
||||
import 'package:zapstore/services/trusted_signers_service.dart';
|
||||
import 'package:zapstore/utils/extensions.dart';
|
||||
import 'package:zapstore/widgets/author_container.dart';
|
||||
@@ -544,7 +545,8 @@ class InstallButton extends ConsumerWidget {
|
||||
|
||||
if (shouldShowDialog) {
|
||||
if (!context.mounted) return false;
|
||||
final result = await showBaseDialog<({bool trustPermanently})>(
|
||||
final result = await showBaseDialog<
|
||||
({bool trustPermanently, bool alwaysUseCdn})>(
|
||||
context: context,
|
||||
dialog: InstallAlertDialog(app: app),
|
||||
);
|
||||
@@ -554,6 +556,12 @@ class InstallButton extends ConsumerWidget {
|
||||
await ref.read(trustServiceProvider).addTrustedSigner(signerPubkey);
|
||||
} catch (_) {}
|
||||
}
|
||||
try {
|
||||
await ref
|
||||
.read(secureStorageServiceProvider)
|
||||
.setAlwaysUseCdn(result.alwaysUseCdn);
|
||||
ref.invalidate(alwaysUseCdnProvider);
|
||||
} catch (_) {}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user