Add AdMob

This commit is contained in:
Ming Ming 2021-03-27 17:36:54 +08:00
parent cfc1bf34ec
commit e1bfca432e
22 changed files with 538 additions and 6 deletions

View file

@ -59,5 +59,8 @@
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="" />
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="" />
</application>
</manifest>

19
app/lib/ad_helper.dart Normal file
View file

@ -0,0 +1,19 @@
import 'package:np_platform_util/np_platform_util.dart';
class AdHelper {
static String get bannerAdUnitId {
if (getRawPlatform() == NpPlatform.android) {
return "";
} else {
throw UnsupportedError("Unsupported platform");
}
}
static String get rewardedAdUnitId {
if (getRawPlatform() == NpPlatform.android) {
return "";
} else {
throw UnsupportedError("Unsupported platform");
}
}
}

View file

@ -1,6 +1,7 @@
import 'package:equatable/equatable.dart';
import 'package:event_bus/event_bus.dart';
import 'package:flutter/foundation.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:kiwi/kiwi.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/di_container.dart';
@ -84,6 +85,8 @@ Future<void> init(InitIsolateType isolateType) async {
// init session storage
SessionStorage();
await _initAds();
_hasInitedInThisIsolate = true;
}
@ -229,5 +232,9 @@ Future<Pref> _createSecurePref() async {
return Pref.scoped(provider);
}
Future<InitializationStatus> _initAds() {
return MobileAds.instance.initialize();
}
final _log = Logger("app_init");
var _hasInitedInThisIsolate = false;

View file

@ -118,6 +118,7 @@ enum PrefKey implements PrefKeyInterface {
viewerAppBarButtons,
viewerBottomAppBarButtons,
homeCollectionsNavBarButtons,
lastAdRewardTime,
;
@override
@ -218,6 +219,8 @@ enum PrefKey implements PrefKeyInterface {
return "viewerBottomAppBarButtons";
case PrefKey.homeCollectionsNavBarButtons:
return "homeCollectionsNavBarButtons";
case PrefKey.lastAdRewardTime:
return "lastAdRewardTime";
}
}
}

View file

@ -165,6 +165,13 @@ extension PrefExtension on Pref {
(key, value) => provider.setBool(key, value));
bool? isNewHttpEngine() => provider.getBool(PrefKey.isNewHttpEngine);
int? getLastAdRewardTime() => provider.getInt(PrefKey.lastAdRewardTime);
int getLastAdRewardTimeOr(int def) => getLastAdRewardTime() ?? def;
Future<bool> setLastAdRewardTime(int value) => _set<int>(
PrefKey.lastAdRewardTime,
value,
(key, value) => provider.setInt(key, value));
}
extension AccountPrefExtension on AccountPref {

View file

@ -4,3 +4,5 @@ final isSupportMapView =
[NpPlatform.android, NpPlatform.web].contains(getRawPlatform());
final isSupportSelfSignedCert = getRawPlatform() == NpPlatform.android;
final isSupportEnhancement = getRawPlatform() == NpPlatform.android;
final isSupportAds = getRawPlatform() != NpPlatform.web;

217
app/lib/widget/ad.dart Normal file
View file

@ -0,0 +1,217 @@
import 'package:flutter/material.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/ad_helper.dart';
import 'package:nc_photos/entity/pref.dart';
import 'package:nc_photos/url_launcher_util.dart';
class AdBanner extends StatefulWidget {
const AdBanner({
Key? key,
}) : super(key: key);
@override
createState() => AdBannerState();
}
class AdBannerState extends State<AdBanner> {
@override
initState() {
super.initState();
_initPlaceholder();
}
@override
build(BuildContext context) {
if (!_isAdLoading) {
_isAdLoading = true;
_createBanner(context);
}
return _buildAdContent(context);
}
@override
dispose() {
super.dispose();
_ad?.dispose();
}
/// Set the height of the placeholder widget
void _initPlaceholder() {
WidgetsBinding.instance.addPostFrameCallback((_) {
_getAdSize().then((size) {
_log.info(
"[_initPlaceholder] Placeholder size: ${size?.width}x${size?.height}");
if (size != null) {
setState(() {
_placeholderHeight = size.height;
});
}
});
});
}
Widget _buildAdContent(BuildContext context) {
if (_isLoadFailed) {
return Container(
alignment: Alignment.center,
height: _placeholderHeight?.toDouble() ?? 77,
padding: const EdgeInsets.symmetric(horizontal: 16),
color: Theme.of(context).scaffoldBackgroundColor,
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
const Text("Tired of ads?"),
Expanded(
child: Align(
alignment: AlignmentDirectional.centerEnd,
child: ElevatedButton(
onPressed: () {
launch(
"https://play.google.com/store/apps/details?id=com.nkming.nc_photos.paid&referrer=utm_source%3Dfreeapp");
},
child: const Text("SUPPORT THE APP"),
),
),
),
],
),
);
} else if (_ad != null) {
return Container(
alignment: Alignment.center,
width: _ad!.size.width.toDouble(),
height: _ad!.size.height.toDouble(),
child: AdWidget(ad: _ad!),
);
} else {
if (_placeholderHeight == null) {
return const SizedBox.shrink();
} else {
return SizedBox(height: _placeholderHeight!.toDouble());
}
}
}
Future<void> _createBanner(BuildContext context) async {
AdSize? size = await _getAdSize();
if (size == null) {
_log.severe("[_createBanner] Unable to get size of adaptive banner");
size = AdSize.banner;
}
final BannerAd banner = BannerAd(
size: size,
request: _request,
adUnitId: AdHelper.bannerAdUnitId,
listener: BannerAdListener(
onAdLoaded: (ad) {
setState(() {
_log.fine("[_createBanner] Ad loaded");
_ad = ad as BannerAd;
_log.fine(
"[_createBanner] Size: ${_ad!.size.width} * ${_ad!.size.height}");
});
},
onAdFailedToLoad: (ad, e) {
_log.shout("[_createBanner] Failed while loading ads", e);
ad.dispose();
setState(() {
_isLoadFailed = true;
});
},
),
);
return banner.load();
}
Future<AdSize?> _getAdSize() => AdSize.getAnchoredAdaptiveBannerAdSize(
Orientation.portrait,
MediaQuery.of(context).size.shortestSide.truncate(),
);
BannerAd? _ad;
var _isAdLoading = false;
var _isLoadFailed = false;
int? _placeholderHeight;
static final _log = Logger("widget.ad.AdBannerState");
}
class RewardedAdHandler {
static bool isCooldownPeriod(Duration cooldown) {
final lastEpoch = Pref().getLastAdRewardTime();
if (lastEpoch == null) {
return false;
}
final last = DateTime.fromMillisecondsSinceEpoch(lastEpoch).toUtc();
final now = DateTime.now().toUtc();
return last.isBefore(now) && now.difference(last) < cooldown;
}
void init() {
_log.info("[init] Start loading ad");
_createRewarded();
}
void show({
VoidCallback? onAdDismissedFullScreenContent,
void Function(AdError error)? onAdFailedToShowFullScreenContent,
void Function(RewardItem reward)? onUserEarnedReward,
}) {
if (!isAdReady) {
_log.warning("[show] Ad is not ready");
return;
}
_ad!.fullScreenContentCallback = FullScreenContentCallback(
onAdDismissedFullScreenContent: (ad) {
_log.info("[show] onAdDismissedFullScreenContent");
ad.dispose();
onAdDismissedFullScreenContent?.call();
},
onAdFailedToShowFullScreenContent: (ad, e) {
_log.severe("[show] onAdFailedToShowFullScreenContent", e);
ad.dispose();
onAdFailedToShowFullScreenContent?.call(e);
},
);
_ad!.setImmersiveMode(true);
_ad!.show(
onUserEarnedReward: (ad, reward) {
_log.info("[show] onUserEarnedReward");
onUserEarnedReward?.call(reward);
},
);
_ad = null;
}
bool get isAdReady => _ad != null;
bool get isGood => _loadAttempts < 3;
void _createRewarded() {
RewardedAd.load(
adUnitId: AdHelper.rewardedAdUnitId,
request: _request,
rewardedAdLoadCallback: RewardedAdLoadCallback(
onAdLoaded: (ad) {
_ad = ad;
_loadAttempts = 0;
},
onAdFailedToLoad: (e) {
_log.shout("[_createRewarded] Failed while loading ads", e);
++_loadAttempts;
if (isGood) {
_createRewarded();
}
},
),
);
}
RewardedAd? _ad;
var _loadAttempts = 0;
static final _log = Logger("widget.ad.RewardedAdHandler");
}
const _request = AdRequest();

View file

@ -41,9 +41,11 @@ import 'package:nc_photos/gps_map_util.dart';
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/np_api_util.dart';
import 'package:nc_photos/object_extension.dart';
import 'package:nc_photos/platform/features.dart' as features;
import 'package:nc_photos/session_storage.dart';
import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/stream_util.dart';
import 'package:nc_photos/widget/ad.dart';
import 'package:nc_photos/widget/album_share_outlier_browser.dart';
import 'package:nc_photos/widget/app_bar_circular_progress_indicator.dart';
import 'package:nc_photos/widget/app_intermediate_circular_progress_indicator.dart';
@ -336,6 +338,13 @@ class _WrappedCollectionBrowserState extends State<_WrappedCollectionBrowser>
: const SizedBox(height: 4),
),
),
if (features.isSupportAds)
const SliverPadding(
padding: EdgeInsets.symmetric(vertical: 8),
sliver: SliverToBoxAdapter(
child: AdBanner(),
),
),
_BlocBuilder(
buildWhen: (previous, current) =>
previous.isEditMode != current.isEditMode ||

View file

@ -0,0 +1,114 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/entity/pref.dart';
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/platform/features.dart' as features;
import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/url_launcher_util.dart';
import 'package:nc_photos/widget/ad.dart';
import 'package:np_math/np_math.dart';
class AdGateHandler {
AdGateHandler() {
if (features.isSupportAds && !_isCooldown()) {
_hasLoadedAd = true;
_adHandler.init();
} else {
_hasLoadedAd = false;
}
}
/// Handle ad-gated contents
///
/// Return true if this user can proceed to the contents
Future<bool> call({
required BuildContext context,
required String contentText,
required String rewardedText,
}) async {
// check cooldown again since the ads may get shown in other instances
if (!_hasLoadedAd || _isCooldown()) {
return true;
}
final result = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
content: Text(contentText),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(true);
},
child: const Text("WATCH AD"),
),
ElevatedButton(
onPressed: () {
launch(
"https://play.google.com/store/apps/details?id=com.nkming.nc_photos.paid&referrer=utm_source%3Dfreeapp");
},
child: const Text("GET PAID VERSION"),
),
],
),
);
if (result != true) {
return false;
} else {
return await _showAd(rewardedText);
}
}
bool _isCooldown() =>
RewardedAdHandler.isCooldownPeriod(const Duration(days: 1));
Future<bool> _showAd(String rewardedText) async {
// wait for the ad to finish loading, max 5s
for (final _ in 0.until(50)) {
if (_adHandler.isAdReady) {
break;
}
await Future.delayed(const Duration(milliseconds: 100));
}
if (!_adHandler.isAdReady) {
_log.shout("[_showAd] Ad failed to load in time");
SnackBarManager().showSnackBar(const SnackBar(
content: Text("Failed to load ad"),
duration: k.snackBarDurationNormal,
));
return false;
}
final dismissCompleter = Completer();
bool isEarned = false;
_adHandler.show(
onAdDismissedFullScreenContent: () {
dismissCompleter.complete();
},
onAdFailedToShowFullScreenContent: (_) {
dismissCompleter.complete();
},
onUserEarnedReward: (_) {
isEarned = true;
},
);
await dismissCompleter.future;
if (isEarned) {
await Pref().setLastAdRewardTime(DateTime.now().millisecondsSinceEpoch);
SnackBarManager().showSnackBar(SnackBar(
content: Text(rewardedText),
duration: k.snackBarDurationNormal,
));
return true;
} else {
return false;
}
}
final _adHandler = RewardedAdHandler();
late final bool _hasLoadedAd;
static final _log = Logger("widget.handler.ad_gate_handler.AdGateHandler");
}

View file

@ -29,6 +29,7 @@ import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/stream_util.dart';
import 'package:nc_photos/theme.dart';
import 'package:nc_photos/theme/dimension.dart';
import 'package:nc_photos/widget/ad.dart';
import 'package:nc_photos/widget/album_importer.dart';
import 'package:nc_photos/widget/archive_browser.dart';
import 'package:nc_photos/widget/collection_browser.dart';
@ -168,6 +169,13 @@ class _BodyView extends StatelessWidget {
? const _AppBar()
: const _SelectionAppBar(),
),
if (features.isSupportAds)
const SliverPadding(
padding: EdgeInsets.symmetric(vertical: 8),
sliver: SliverToBoxAdapter(
child: AdBanner(),
),
),
const SliverToBoxAdapter(
child: SizedBox(height: 8),
),

View file

@ -0,0 +1,20 @@
part of '../home_photos2.dart';
class _BannerAd extends StatelessWidget {
const _BannerAd();
@override
Widget build(BuildContext context) {
return SliverMeasureExtent(
onChange: (extent) {
context.addEvent(_UpdateBannerAdExtent(extent));
},
child: const SliverPadding(
padding: EdgeInsets.symmetric(vertical: 8),
sliver: SliverToBoxAdapter(
child: AdBanner(),
),
),
);
}
}

View file

@ -52,6 +52,8 @@ class _Bloc extends Bloc<_Event, _State>
on<_TripMissingVideoPreview>(_onTripMissingVideoPreview);
on<_UpdateBannerAdExtent>(_onUpdateBannerAdExtent);
on<_SetError>(_onSetError);
_subscriptions
@ -372,7 +374,8 @@ class _Bloc extends Bloc<_Event, _State>
itemPerRow: state.itemPerRow!,
viewHeight: state.viewHeight!,
);
final totalHeight = minimapItems.map((e) => e.logicalHeight).sum;
final totalHeight = (state.bannerAdExtent ?? 0) +
minimapItems.map((e) => e.logicalHeight).sum;
final ratio = state.viewHeight! / totalHeight;
_log.info(
"[_onTransformMinimap] view height: ${state.viewHeight!}, logical height: $totalHeight");
@ -492,6 +495,11 @@ class _Bloc extends Bloc<_Event, _State>
}
}
void _onUpdateBannerAdExtent(_UpdateBannerAdExtent ev, Emitter<_State> emit) {
_log.info(ev);
emit(state.copyWith(bannerAdExtent: ev.value));
}
void _onSetError(_SetError ev, Emitter<_State> emit) {
_log.info(ev);
emit(state.copyWith(error: ExceptionEvent(ev.error, ev.stackTrace)));

View file

@ -36,6 +36,9 @@ class _MinimapView extends StatelessWidget {
// prevent overlap
top = prevItemY! + _minDateY;
}
// add extra padding for the banner ads
top +=
(state.bannerAdExtent ?? 0) * state.minimapYRatio;
prevItemY = top;
final text =
"${DateFormat.y().format(prevDate!.toLocalDateTime())}";

View file

@ -26,6 +26,7 @@ class _State {
required this.minimapYRatio,
this.scrollDate,
required this.hasMissingVideoPreview,
this.bannerAdExtent,
this.error,
});
@ -80,6 +81,8 @@ class _State {
final bool hasMissingVideoPreview;
final double? bannerAdExtent;
final ExceptionEvent? error;
}
@ -317,6 +320,15 @@ class _TripMissingVideoPreview implements _Event {
String toString() => _$toString();
}
class _UpdateBannerAdExtent implements _Event {
const _UpdateBannerAdExtent(this.value);
@override
String toString() => _$toString();
final double? value;
}
@toString
class _SetError implements _Event {
const _SetError(this.error, [this.stackTrace]);

View file

@ -36,6 +36,7 @@ import 'package:nc_photos/exception_event.dart';
import 'package:nc_photos/flutter_util.dart' as flutter_util;
import 'package:nc_photos/help_utils.dart' as help_util;
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/platform/features.dart' as features;
import 'package:nc_photos/progress_util.dart';
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
import 'package:nc_photos/session_storage.dart';
@ -44,12 +45,14 @@ import 'package:nc_photos/stream_extension.dart';
import 'package:nc_photos/theme.dart';
import 'package:nc_photos/theme/dimension.dart';
import 'package:nc_photos/url_launcher_util.dart';
import 'package:nc_photos/widget/ad.dart';
import 'package:nc_photos/widget/collection_browser.dart';
import 'package:nc_photos/widget/collection_picker.dart';
import 'package:nc_photos/widget/double_tap_exit_container/double_tap_exit_container.dart';
import 'package:nc_photos/widget/file_sharer_dialog.dart';
import 'package:nc_photos/widget/finger_listener.dart';
import 'package:nc_photos/widget/home_app_bar.dart';
import 'package:nc_photos/widget/measure.dart';
import 'package:nc_photos/widget/navigation_bar_blur_filter.dart';
import 'package:nc_photos/widget/network_thumbnail.dart';
import 'package:nc_photos/widget/photo_list_item.dart';
@ -69,6 +72,7 @@ import 'package:np_ui/np_ui.dart';
import 'package:to_string/to_string.dart';
import 'package:visibility_detector/visibility_detector.dart';
part 'home_photos/ads.dart';
part 'home_photos/app_bar.dart';
part 'home_photos/bloc.dart';
part 'home_photos/minimap_view.dart';
@ -329,7 +333,8 @@ class _BodyState extends State<_Body> {
(previous.isEnableMemoryCollection &&
previous.memoryCollections.isNotEmpty) !=
(current.isEnableMemoryCollection &&
current.memoryCollections.isNotEmpty),
current.memoryCollections.isNotEmpty) ||
previous.bannerAdExtent != current.bannerAdExtent,
builder: (context, state) {
final scrollExtent = _getScrollViewExtent(
context: context,
@ -392,6 +397,7 @@ class _BodyState extends State<_Body> {
? const _AppBar()
: const _SelectionAppBar(),
),
if (features.isSupportAds) const _BannerAd(),
_BlocBuilder(
buildWhen: (previous, current) =>
(previous.isEnableMemoryCollection &&
@ -479,7 +485,9 @@ class _BodyState extends State<_Body> {
required bool hasMemoryCollection,
required double? contentListMaxExtent,
}) {
if (contentListMaxExtent != null && constraints.hasBoundedHeight) {
if (contentListMaxExtent != null &&
constraints.hasBoundedHeight &&
(!features.isSupportAds || context.state.bannerAdExtent != null)) {
final appBarExtent = _getAppBarExtent(context);
final bottomAppBarExtent =
AppDimension.of(context).homeBottomAppBarHeight;
@ -494,13 +502,15 @@ class _BodyState extends State<_Body> {
appBarExtent +
bottomAppBarExtent +
// metadataTaskHeaderExtent +
smartAlbumListHeight;
smartAlbumListHeight +
(context.state.bannerAdExtent ?? 0);
_log.info("[_getScrollViewExtent] $contentListMaxExtent "
"- ${constraints.maxHeight} "
"+ $appBarExtent "
"+ $bottomAppBarExtent "
// "+ $metadataTaskHeaderExtent "
"+ $smartAlbumListHeight "
"+ ${context.state.bannerAdExtent} "
"= $scrollExtent");
return scrollExtent;
} else {
@ -527,7 +537,7 @@ typedef _BlocSelector<T> = BlocSelector<_Bloc, _State, T>;
extension on BuildContext {
_Bloc get bloc => read<_Bloc>();
// _State get state => bloc.state;
_State get state => bloc.state;
void addEvent(_Event event) => bloc.add(event);
}

View file

@ -36,6 +36,7 @@ abstract class $_StateCopyWithWorker {
double? minimapYRatio,
Date? scrollDate,
bool? hasMissingVideoPreview,
double? bannerAdExtent,
ExceptionEvent? error});
}
@ -66,6 +67,7 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
dynamic minimapYRatio,
dynamic scrollDate = copyWithNull,
dynamic hasMissingVideoPreview,
dynamic bannerAdExtent = copyWithNull,
dynamic error = copyWithNull}) {
return _State(
files: files as List<FileDescriptor>? ?? that.files,
@ -106,6 +108,9 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
scrollDate == copyWithNull ? that.scrollDate : scrollDate as Date?,
hasMissingVideoPreview:
hasMissingVideoPreview as bool? ?? that.hasMissingVideoPreview,
bannerAdExtent: bannerAdExtent == copyWithNull
? that.bannerAdExtent
: bannerAdExtent as double?,
error: error == copyWithNull ? that.error : error as ExceptionEvent?);
}
@ -184,7 +189,7 @@ extension _$_ContentListBodyNpLog on _ContentListBody {
extension _$_StateToString on _State {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_State {files: [length: ${files.length}], isLoading: $isLoading, transformedItems: [length: ${transformedItems.length}], selectedItems: {length: ${selectedItems.length}}, filesSummary: $filesSummary, visibleDates: {length: ${visibleDates.length}}, queriedDates: {length: ${queriedDates.length}}, isEnableMemoryCollection: $isEnableMemoryCollection, memoryCollections: [length: ${memoryCollections.length}], contentListMaxExtent: ${contentListMaxExtent == null ? null : "${contentListMaxExtent!.toStringAsFixed(3)}"}, syncProgress: $syncProgress, zoom: $zoom, scale: ${scale == null ? null : "${scale!.toStringAsFixed(3)}"}, viewWidth: ${viewWidth == null ? null : "${viewWidth!.toStringAsFixed(3)}"}, viewHeight: ${viewHeight == null ? null : "${viewHeight!.toStringAsFixed(3)}"}, itemPerRow: $itemPerRow, itemSize: ${itemSize == null ? null : "${itemSize!.toStringAsFixed(3)}"}, isScrolling: $isScrolling, minimapItems: ${minimapItems == null ? null : "[length: ${minimapItems!.length}]"}, minimapYRatio: ${minimapYRatio.toStringAsFixed(3)}, scrollDate: $scrollDate, hasMissingVideoPreview: $hasMissingVideoPreview, error: $error}";
return "_State {files: [length: ${files.length}], isLoading: $isLoading, transformedItems: [length: ${transformedItems.length}], selectedItems: {length: ${selectedItems.length}}, filesSummary: $filesSummary, visibleDates: {length: ${visibleDates.length}}, queriedDates: {length: ${queriedDates.length}}, isEnableMemoryCollection: $isEnableMemoryCollection, memoryCollections: [length: ${memoryCollections.length}], contentListMaxExtent: ${contentListMaxExtent == null ? null : "${contentListMaxExtent!.toStringAsFixed(3)}"}, syncProgress: $syncProgress, zoom: $zoom, scale: ${scale == null ? null : "${scale!.toStringAsFixed(3)}"}, viewWidth: ${viewWidth == null ? null : "${viewWidth!.toStringAsFixed(3)}"}, viewHeight: ${viewHeight == null ? null : "${viewHeight!.toStringAsFixed(3)}"}, itemPerRow: $itemPerRow, itemSize: ${itemSize == null ? null : "${itemSize!.toStringAsFixed(3)}"}, isScrolling: $isScrolling, minimapItems: ${minimapItems == null ? null : "[length: ${minimapItems!.length}]"}, minimapYRatio: ${minimapYRatio.toStringAsFixed(3)}, scrollDate: $scrollDate, hasMissingVideoPreview: $hasMissingVideoPreview, bannerAdExtent: ${bannerAdExtent == null ? null : "${bannerAdExtent!.toStringAsFixed(3)}"}, error: $error}";
}
}
@ -364,6 +369,13 @@ extension _$_TripMissingVideoPreviewToString on _TripMissingVideoPreview {
}
}
extension _$_UpdateBannerAdExtentToString on _UpdateBannerAdExtent {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_UpdateBannerAdExtent {value: ${value == null ? null : "${value!.toStringAsFixed(3)}"}}";
}
}
extension _$_SetErrorToString on _SetError {
String _$toString() {
// ignore: unnecessary_string_interpolations

View file

@ -17,6 +17,7 @@ import 'package:nc_photos/np_api_util.dart';
import 'package:nc_photos/object_extension.dart';
import 'package:nc_photos/theme.dart';
import 'package:nc_photos/url_launcher_util.dart';
import 'package:nc_photos/widget/handler/ad_gate_handler.dart';
import 'package:nc_photos/widget/handler/permission_handler.dart';
import 'package:nc_photos/widget/image_editor/color_toolbar.dart';
import 'package:nc_photos/widget/image_editor/crop_controller.dart';
@ -286,6 +287,15 @@ class _ImageEditorState extends State<ImageEditor> {
}
Future<void> _onSavePressed(BuildContext context) async {
if (!await _adGateHandler(
context: context,
contentText:
"Photo editing is a premium feature but you can unlock it by watching an ad. Once unlocked it will last for 1 day.",
rewardedText: "Your photo will now be processed in the background",
)) {
return;
}
final c = KiwiContainer().resolve<DiContainer>();
await ImageProcessor.filter(
"${widget.account.url}/${widget.file.fdPath}",
@ -344,6 +354,8 @@ class _ImageEditorState extends State<ImageEditor> {
var _colorFilters = <ColorArguments>[];
var _transformFilters = <TransformArguments>[];
TransformArguments? _cropFilter;
final _adGateHandler = AdGateHandler();
}
enum _ToolType {

View file

@ -24,6 +24,7 @@ import 'package:nc_photos/object_extension.dart';
import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/theme.dart';
import 'package:nc_photos/url_launcher_util.dart';
import 'package:nc_photos/widget/handler/ad_gate_handler.dart';
import 'package:nc_photos/widget/handler/permission_handler.dart';
import 'package:nc_photos/widget/image_editor_persist_option_dialog.dart';
import 'package:nc_photos/widget/selectable.dart';
@ -193,6 +194,14 @@ class _ImageEnhancerState extends State<ImageEnhancer> {
// user canceled
return;
}
if (!await _adGateHandler(
context: context,
contentText:
"Photo enhancement is a premium feature but you can unlock it by watching an ad. Once unlocked it will last for 1 day.",
rewardedText: "Your photo will now be processed in the background",
)) {
return;
}
switch (_selectedOption.algorithm) {
case _Algorithm.zeroDce:
await ImageProcessor.zeroDce(
@ -585,6 +594,8 @@ class _ImageEnhancerState extends State<ImageEnhancer> {
late final DiContainer _c;
late var _selectedOption = _options[0];
late final _pageController = PageController(keepPage: false);
final _adGateHandler = AdGateHandler();
}
enum _Algorithm {

View file

@ -18,11 +18,13 @@ import 'package:nc_photos/exception.dart';
import 'package:nc_photos/exception_event.dart';
import 'package:nc_photos/help_utils.dart' as help_util;
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/platform/features.dart' as features;
import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/stream_util.dart';
import 'package:nc_photos/theme.dart';
import 'package:nc_photos/url_launcher_util.dart';
import 'package:nc_photos/use_case/list_location_group.dart';
import 'package:nc_photos/widget/ad.dart';
import 'package:nc_photos/widget/app_intermediate_circular_progress_indicator.dart';
import 'package:nc_photos/widget/collection_browser.dart';
import 'package:nc_photos/widget/network_thumbnail.dart';
@ -141,6 +143,11 @@ class _WrappedSearchLandingState extends State<_WrappedSearchLanding> {
],
child: Column(
children: [
if (features.isSupportAds)
const Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: AdBanner(),
),
ValueStreamBuilder<PersonProvider>(
stream: context
.read<AccountController>()

View file

@ -29,6 +29,7 @@ import 'package:nc_photos/use_case/inflate_file_descriptor.dart';
import 'package:nc_photos/use_case/list_file_tag.dart';
import 'package:nc_photos/use_case/update_property.dart';
import 'package:nc_photos/widget/about_geocoding_dialog.dart';
import 'package:nc_photos/widget/ad.dart';
import 'package:nc_photos/widget/handler/add_selection_to_collection_handler.dart';
import 'package:nc_photos/widget/list_tile_center_leading.dart';
import 'package:nc_photos/widget/photo_date_time_edit_dialog.dart';
@ -258,6 +259,11 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
child: Divider(),
),
],
if (features.isSupportAds)
const Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: AdBanner(),
),
ListTile(
leading: const ListTileCenterLeading(
child: Icon(Icons.image_outlined),

View file

@ -748,6 +748,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.5.4+3"
google_mobile_ads:
dependency: "direct main"
description:
name: google_mobile_ads
sha256: e2d18992d30b2be77cb6976b931112fc3c4612feffb5eb7a8b036bd7a64934da
url: "https://pub.dev"
source: hosted
version: "5.1.0"
graphs:
dependency: transitive
description:
@ -1998,6 +2006,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.1"
webview_flutter:
dependency: transitive
description:
name: webview_flutter
sha256: "6869c8786d179f929144b4a1f86e09ac0eddfe475984951ea6c634774c16b522"
url: "https://pub.dev"
source: hosted
version: "4.8.0"
webview_flutter_android:
dependency: transitive
description:
name: webview_flutter_android
sha256: "0d21cfc3bfdd2e30ab2ebeced66512b91134b39e72e97b43db2d47dda1c4e53a"
url: "https://pub.dev"
source: hosted
version: "3.16.3"
webview_flutter_platform_interface:
dependency: transitive
description:
name: webview_flutter_platform_interface
sha256: d937581d6e558908d7ae3dc1989c4f87b786891ab47bb9df7de548a151779d8d
url: "https://pub.dev"
source: hosted
version: "2.10.0"
webview_flutter_wkwebview:
dependency: transitive
description:
name: webview_flutter_wkwebview
sha256: "7affdf9d680c015b11587181171d3cad8093e449db1f7d9f0f08f4f33d24f9a0"
url: "https://pub.dev"
source: hosted
version: "3.13.1"
win32:
dependency: transitive
description:

View file

@ -161,6 +161,8 @@ dependencies:
visibility_detector: 0.3.3
wakelock_plus: ^1.1.1
woozy_search: ^2.0.3
# android/ios only
google_mobile_ads: 5.1.0
dependency_overrides:
video_player: