mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-22 16:56:19 +01:00
Add AdMob
This commit is contained in:
parent
cfc1bf34ec
commit
e1bfca432e
22 changed files with 538 additions and 6 deletions
|
@ -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
19
app/lib/ad_helper.dart
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
217
app/lib/widget/ad.dart
Normal 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();
|
|
@ -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 ||
|
||||
|
|
114
app/lib/widget/handler/ad_gate_handler.dart
Normal file
114
app/lib/widget/handler/ad_gate_handler.dart
Normal 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");
|
||||
}
|
|
@ -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),
|
||||
),
|
||||
|
|
20
app/lib/widget/home_photos/ads.dart
Normal file
20
app/lib/widget/home_photos/ads.dart
Normal 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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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)));
|
||||
|
|
|
@ -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())} —";
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>()
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue