Files
PinePods-nix/PinePods-0.8.2/mobile/lib/ui/podcast/funding_menu.dart
2026-03-03 10:57:43 -05:00

223 lines
7.4 KiB
Dart

// Copyright 2020 Ben Hills and the project contributors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:pinepods_mobile/bloc/settings/settings_bloc.dart';
import 'package:pinepods_mobile/entities/app_settings.dart';
import 'package:pinepods_mobile/entities/funding.dart';
import 'package:pinepods_mobile/l10n/L.dart';
import 'package:pinepods_mobile/ui/widgets/action_text.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dialogs/flutter_dialogs.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
/// This class is responsible for rendering the funding menu on the podcast details page.
///
/// It returns either a Material or Cupertino style menu instance depending upon which
/// platform we are running on.
///
/// The target platform is based on the current [Theme]: [ThemeData.platform].
class FundingMenu extends StatelessWidget {
final List<Funding>? funding;
const FundingMenu(
this.funding, {
super.key,
});
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
switch (theme.platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return _MaterialFundingMenu(funding);
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return _CupertinoFundingMenu(funding);
}
}
}
/// This is the material design version of the context menu. This will be rendered
/// for all platforms that are not iOS.
class _MaterialFundingMenu extends StatelessWidget {
final List<Funding>? funding;
const _MaterialFundingMenu(this.funding);
@override
Widget build(BuildContext context) {
final settingsBloc = Provider.of<SettingsBloc>(context);
return funding == null || funding!.isEmpty
? const SizedBox(
width: 0.0,
height: 0.0,
)
: StreamBuilder<AppSettings>(
stream: settingsBloc.settings,
initialData: AppSettings.sensibleDefaults(),
builder: (context, snapshot) {
return Semantics(
label: L.of(context)!.podcast_funding_dialog_header,
child: PopupMenuButton<String>(
onSelected: (url) {
FundingLink.fundingLink(
url,
snapshot.data!.externalLinkConsent,
context,
).then((value) {
settingsBloc.setExternalLinkConsent(value);
});
},
icon: const Icon(
Icons.payment,
),
itemBuilder: (BuildContext context) {
return List<PopupMenuEntry<String>>.generate(funding!.length, (index) {
return PopupMenuItem<String>(
value: funding![index].url,
enabled: true,
child: Text(funding![index].value),
);
});
},
),
);
});
}
}
/// This is the Cupertino context menu and is rendered only when running on
/// an iOS device.
class _CupertinoFundingMenu extends StatelessWidget {
final List<Funding>? funding;
const _CupertinoFundingMenu(this.funding);
@override
Widget build(BuildContext context) {
final settingsBloc = Provider.of<SettingsBloc>(context);
return funding == null || funding!.isEmpty
? const SizedBox(
width: 0.0,
height: 0.0,
)
: StreamBuilder<AppSettings>(
stream: settingsBloc.settings,
initialData: AppSettings.sensibleDefaults(),
builder: (context, snapshot) {
return IconButton(
tooltip: L.of(context)!.podcast_funding_dialog_header,
icon: const Icon(Icons.payment),
visualDensity: VisualDensity.compact,
onPressed: () => showCupertinoModalPopup<void>(
context: context,
builder: (BuildContext context) {
return CupertinoActionSheet(
actions: <Widget>[
...List<CupertinoActionSheetAction>.generate(funding!.length, (index) {
return CupertinoActionSheetAction(
onPressed: () {
FundingLink.fundingLink(
funding![index].url,
snapshot.data!.externalLinkConsent,
context,
).then((value) {
settingsBloc.setExternalLinkConsent(value);
if (context.mounted) {
Navigator.of(context).pop('Cancel');
}
});
},
child: Text(funding![index].value),
);
}),
],
cancelButton: CupertinoActionSheetAction(
isDefaultAction: true,
onPressed: () {
Navigator.pop(context, 'Cancel');
},
child: Text(L.of(context)!.cancel_option_label),
),
);
},
),
);
});
}
}
class FundingLink {
/// Check the consent status. If this is the first time we have been
/// requested to open a funding link, present the user with and
/// information dialog first to make clear that the link is provided
/// by the podcast owner and not Pinepods.
static Future<bool> fundingLink(String url, bool consent, BuildContext context) async {
bool? result = false;
if (consent) {
result = true;
final uri = Uri.parse(url);
if (!await launchUrl(
uri,
mode: LaunchMode.externalApplication,
)) {
throw Exception('Could not launch $uri');
}
} else {
result = await showPlatformDialog<bool>(
context: context,
useRootNavigator: false,
builder: (_) => BasicDialogAlert(
title: Semantics(
header: true,
child: Text(L.of(context)!.podcast_funding_dialog_header),
),
content: Text(L.of(context)!.consent_message),
actions: <Widget>[
BasicDialogAction(
title: ActionText(
L.of(context)!.go_back_button_label,
),
onPressed: () {
Navigator.pop(context, false);
},
),
BasicDialogAction(
title: ActionText(
L.of(context)!.continue_button_label,
),
iosIsDefaultAction: true,
onPressed: () {
Navigator.pop(context, true);
},
),
],
),
);
if (result!) {
var uri = Uri.parse(url);
unawaited(
canLaunchUrl(uri).then((value) => launchUrl(uri)),
);
}
}
return Future.value(result);
}
}