pubspec.yaml
name: cidr
description: "CIDR"
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 2.0.1+20
environment:
sdk: ^3.9.2
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
google_mobile_ads: ^6.0.0
shared_preferences: ^2.2.3
intl: ^0.20.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter_launcher_icons: ^0.14.3 #flutter pub run flutter_launcher_icons
flutter_native_splash: ^2.3.6 #flutter pub run flutter_native_splash:create
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^6.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
flutter_icons:
android: "launcher_icon"
ios: true
image_path: "assets/icon/icon.png"
adaptive_icon_background: "assets/icon/icon_back.png"
adaptive_icon_foreground: "assets/icon/icon_fore.png"
flutter_native_splash:
color: '#9da9f5'
image: 'assets/image/splash.png'
color_dark: '#9da9f5'
image_dark: 'assets/image/splash.png'
fullscreen: true
android_12:
icon_background_color: '#9da9f5'
image: 'assets/image/splash.png'
icon_background_color_dark: '#9da9f5'
image_dark: 'assets/image/splash.png'
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package
lib/ad_banner_widget.dart
import 'package:flutter/cupertino.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:cidr/ad_manager.dart';
class AdBannerWidget extends StatefulWidget {
final AdManager adManager;
const AdBannerWidget({super.key, required this.adManager});
@override
State<AdBannerWidget> createState() => _AdBannerWidgetState();
}
class _AdBannerWidgetState extends State<AdBannerWidget> {
int _lastBannerWidthDp = 0;
bool _isAdLoaded = false;
bool _isLoading = false;
@override
Widget build(BuildContext context) {
return SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
final int width = constraints.maxWidth.isFinite ? constraints.maxWidth.truncate() : MediaQuery.of(context).size.width.truncate();
final bannerAd = widget.adManager.bannerAd;
if (width > 0) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
final bannerAd = widget.adManager.bannerAd;
final bool widthChanged = _lastBannerWidthDp != width;
final bool sizeMismatch = bannerAd == null || bannerAd.size.width != width;
if ((widthChanged || !_isAdLoaded || sizeMismatch) && !_isLoading) {
_lastBannerWidthDp = width;
setState(() { _isAdLoaded = false; _isLoading = true; });
widget.adManager.loadAdaptiveBannerAd(width, () {
if (mounted) {
setState(() { _isAdLoaded = true; _isLoading = false; });
}
});
}
}
});
}
if (_isAdLoaded && bannerAd != null) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: bannerAd.size.width.toDouble(),
height: bannerAd.size.height.toDouble(),
child: AdWidget(ad: bannerAd),
),
],
);
} else {
return const SizedBox.shrink();
}
},
),
);
}
}
lib/ad_manager.dart
/*
* mainへの記述
* void main() async {
* WidgetsFlutterBinding.ensureInitialized();
* if (!kIsWeb) {
* //AdMob初期化
* MobileAds.instance.initialize();
* //NPAポリシーの集中設定(将来拡張もここで) 現時点は使用していないので記述しなくても良い
* await AdManager.initForNPA();
* }
* runApp(const MyApp());
* }
*/
import 'dart:async';
import 'dart:io' show Platform;
import 'dart:ui';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:google_mobile_ads/google_mobile_ads.dart';
class AdManager {
//Test IDs
//static const String _androidAdUnitId = "ca-app-pub-3940256099942544/6300978111";
//static const String _iosAdUnitId = "ca-app-pub-3940256099942544/2934735716";
//Production IDs
static const String _androidAdUnitId = "ca-app-pub-0/0";
static const String _iosAdUnitId = "ca-app-pub-0/0";
static String get _adUnitId => Platform.isIOS ? _iosAdUnitId : _androidAdUnitId;
BannerAd? _bannerAd;
int _lastWidthPx = 0;
VoidCallback? _onLoadedCb;
Timer? _retryTimer;
int _retryAttempt = 0;
BannerAd? get bannerAd => _bannerAd;
//(任意)アプリ起動時などに呼ぶ。将来のCMP/NPA関連設定を集中管理。
static Future<void> initForNPA() async {
if (kIsWeb) {
return;
}
//ここでグローバルなRequestConfigurationを設定しておく(必要に応じて拡張)
await MobileAds.instance.updateRequestConfiguration(
RequestConfiguration(
//例:最大コンテンツレーティング等を付けたい場合はここに追加
//maxAdContentRating: MaxAdContentRating.g, //例
//tagForChildDirectedTreatment: TagForChildDirectedTreatment.unspecified,
//tagForUnderAgeOfConsent: TagForUnderAgeOfConsent.unspecified,
),
);
}
Future<void> loadAdaptiveBannerAd(
int widthPx,
VoidCallback onAdLoaded,
) async {
if (kIsWeb) {
return;
}
_onLoadedCb = onAdLoaded;
_lastWidthPx = widthPx;
_retryAttempt = 0;
_retryTimer?.cancel();
_startLoad(widthPx);
}
Future<void> _startLoad(int widthPx) async {
if (kIsWeb) {
return;
}
_bannerAd?.dispose();
AnchoredAdaptiveBannerAdSize? adaptiveSize;
try {
adaptiveSize =
await AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize(
widthPx,
);
} catch (_) {
adaptiveSize = null;
}
final AdSize size = adaptiveSize ?? AdSize.fullBanner;
//常にNPAで配信(CMP対応)
const adRequest = AdRequest(
nonPersonalizedAds: true, //NPA Non-Personalized Ads(非パーソナライズ広告)指定
);
_bannerAd = BannerAd(
adUnitId: _adUnitId,
request: adRequest,
size: size,
listener: BannerAdListener(
onAdLoaded: (ad) {
_retryTimer?.cancel();
_retryAttempt = 0;
final cb = _onLoadedCb;
if (cb != null) {
cb();
}
},
onAdFailedToLoad: (ad, err) {
ad.dispose();
_scheduleRetry();
},
),
)..load();
}
void _scheduleRetry() {
if (kIsWeb) {
return;
}
_retryTimer?.cancel();
// Exponential backoff: 3s, 6s, 12s, max 30s
_retryAttempt = (_retryAttempt + 1).clamp(1, 5);
final seconds = _retryAttempt >= 4 ? 30 : (3 << (_retryAttempt - 1));
_retryTimer = Timer(Duration(seconds: seconds), () {
_startLoad(_lastWidthPx > 0 ? _lastWidthPx : 320);
});
}
void dispose() {
_bannerAd?.dispose();
_retryTimer?.cancel();
}
}
/*
広告配信について
本アプリでは、Google AdMob を利用して広告を表示しています。
当アプリの広告はすべて「非パーソナライズ広告(NPA)」として配信しており、ユーザーの行動履歴や個人情報をもとにしたパーソナライズは一切行っていません。
Google AdMob によって、広告の表示のために以下の情報が利用される場合があります:
- 端末情報(例:OSの種類、画面サイズなど)
- おおまかな位置情報(国・地域レベル)
これらの情報は、パーソナライズを目的としたトラッキングやプロファイリングには使用されません。
詳しくは、Google のプライバシーポリシーをご覧ください:
https://policies.google.com/privacy
Advertising
This app uses Google AdMob to display advertisements.
All ads in this app are served as non-personalized ads (NPA).
This means that we do not use personal data or user behavior information to personalize the ads you see.
Google AdMob may use certain information in order to display ads properly, such as:
- Device information (e.g., OS type, screen size)
- Approximate location information (country/region level)
This information is not used for tracking or profiling for advertising purposes.
For more details, please refer to Google Privacy Policy:
https://policies.google.com/privacy
*/
lib/ad_ump_status.dart
import 'dart:async';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/widgets.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'l10n/app_localizations.dart';
class AdUmpState {
const AdUmpState({
required this.privacyStatus,
required this.consentStatus,
required this.privacyOptionsRequired,
required this.isChecking,
});
final PrivacyOptionsRequirementStatus privacyStatus;
final ConsentStatus consentStatus;
final bool privacyOptionsRequired;
final bool isChecking;
AdUmpState copyWith({
PrivacyOptionsRequirementStatus? privacyStatus,
ConsentStatus? consentStatus,
bool? privacyOptionsRequired,
bool? isChecking,
}) {
return AdUmpState(
privacyStatus: privacyStatus ?? this.privacyStatus,
consentStatus: consentStatus ?? this.consentStatus,
privacyOptionsRequired:
privacyOptionsRequired ?? this.privacyOptionsRequired,
isChecking: isChecking ?? this.isChecking,
);
}
static const AdUmpState initial = AdUmpState(
privacyStatus: PrivacyOptionsRequirementStatus.unknown,
consentStatus: ConsentStatus.unknown,
privacyOptionsRequired: false,
isChecking: false,
);
}
class UmpConsentController {
UmpConsentController({this.forceEeaForDebug = false});
final bool forceEeaForDebug;
static const List<String> _testDeviceIds = <String>[
'608970392F100B87D62A1174996C952C',
];
ConsentRequestParameters _buildParams() {
if (forceEeaForDebug && _testDeviceIds.isNotEmpty) {
return ConsentRequestParameters(
consentDebugSettings: ConsentDebugSettings(
debugGeography: DebugGeography.debugGeographyEea,
testIdentifiers: _testDeviceIds,
),
);
}
return ConsentRequestParameters();
}
Future<AdUmpState> updateConsentInfo({
AdUmpState current = AdUmpState.initial,
}) async {
if (kIsWeb) {
return current;
}
var state = current.copyWith(isChecking: true);
try {
final ConsentRequestParameters params = _buildParams();
final Completer<AdUmpState> completer = Completer<AdUmpState>();
ConsentInformation.instance.requestConsentInfoUpdate(
params,
() async {
final PrivacyOptionsRequirementStatus requirement =
await ConsentInformation.instance
.getPrivacyOptionsRequirementStatus();
final ConsentStatus consent = await ConsentInformation.instance
.getConsentStatus();
completer.complete(
state.copyWith(
privacyStatus: requirement,
consentStatus: consent,
privacyOptionsRequired:
requirement == PrivacyOptionsRequirementStatus.required,
isChecking: false,
),
);
},
(FormError error) {
completer.complete(
state.copyWith(
privacyStatus: PrivacyOptionsRequirementStatus.unknown,
consentStatus: ConsentStatus.unknown,
privacyOptionsRequired: false,
isChecking: false,
),
);
},
);
state = await completer.future;
return state;
} catch (_) {
return state.copyWith(isChecking: false);
}
}
Future<FormError?> showPrivacyOptions() async {
if (kIsWeb) {
return null;
}
final Completer<FormError?> completer = Completer<FormError?>();
ConsentForm.showPrivacyOptionsForm((FormError? error) {
completer.complete(error);
});
return completer.future;
}
}
extension ConsentStatusL10n on ConsentStatus {
String localized(BuildContext context) {
final AppLocalizations localization = AppLocalizations.of(context);
switch (this) {
case ConsentStatus.obtained:
return localization.cmpConsentStatusObtained;
case ConsentStatus.required:
return localization.cmpConsentStatusRequired;
case ConsentStatus.notRequired:
return localization.cmpConsentStatusNotRequired;
case ConsentStatus.unknown:
return localization.cmpConsentStatusUnknown;
}
}
}
lib/cidr_calculator.dart
class CidrCalculationResult {
CidrCalculationResult({
required this.sanitizedIp,
required this.ipParts,
required this.networkParts,
required this.broadcastParts,
required this.maskParts,
required this.prefix,
required this.hostCount,
required this.networkInt,
required this.broadcastInt,
});
final String sanitizedIp;
final List<int> ipParts;
final List<int> networkParts;
final List<int> broadcastParts;
final List<int> maskParts;
final int prefix;
final int hostCount;
final int networkInt;
final int broadcastInt;
}
class CidrCalculator {
static CidrCalculationResult calculate(String rawIp, int prefix) {
final int normalizedPrefix = prefix.clamp(1, 32).toInt();
final List<int> ipParts = _sanitizeIp(rawIp);
final List<int> maskParts = maskFromPrefix(normalizedPrefix);
final int ipInt = _ipToInt(ipParts);
final int maskInt = _ipToInt(maskParts);
final int wildcardInt = 0xFFFFFFFF ^ maskInt;
final int networkInt = ipInt & maskInt;
final int broadcastInt = networkInt | wildcardInt;
final List<int> networkParts = _intToIp(networkInt);
final List<int> broadcastParts = _intToIp(broadcastInt);
final int hostCount = hostCountForPrefix(normalizedPrefix);
return CidrCalculationResult(
sanitizedIp: formatIp(ipParts),
ipParts: ipParts,
networkParts: networkParts,
broadcastParts: broadcastParts,
maskParts: maskParts,
prefix: normalizedPrefix,
hostCount: hostCount,
networkInt: networkInt,
broadcastInt: broadcastInt,
);
}
static List<int> maskFromPrefix(int prefix) {
final int normalized = prefix.clamp(0, 32).toInt();
final List<int> mask = List<int>.filled(4, 0);
for (int i = 0; i < 4; i++) {
final int bits = normalized - (i * 8);
if (bits >= 8) {
mask[i] = 255;
} else if (bits <= 0) {
mask[i] = 0;
} else {
mask[i] = (0xFF << (8 - bits)) & 0xFF;
}
}
return mask;
}
static int hostCountForPrefix(int prefix) {
final int normalized = prefix.clamp(1, 32).toInt();
return 1 << (32 - normalized);
}
static String formatIp(List<int> parts) => parts.join('.');
static String formatBits(List<int> parts) => parts.map((int p) => p.toRadixString(2).padLeft(8, '0')).join('.');
static List<int> intToIp(int value) => _intToIp(value);
static List<int> _sanitizeIp(String rawIp) {
final List<String> segments = (rawIp.trim().isEmpty ? '' : rawIp.trim()).split('.');
segments.addAll(List<String>.filled(4, '0'));
final List<int> result = List<int>.filled(4, 0);
for (int i = 0; i < 4; i++) {
final String segment = i < segments.length ? segments[i] : '0';
final String digitsOnly = segment.replaceAll(RegExp(r'[^0-9]'), '');
final String trimmed = digitsOnly.length <= 3 ? digitsOnly : digitsOnly.substring(digitsOnly.length - 3);
int value = int.tryParse(trimmed) ?? 0;
if (value < 0) {
value = 0;
} else if (value > 255) {
value = 255;
}
result[i] = value;
}
return result;
}
static int _ipToInt(List<int> parts) {
return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3];
}
static List<int> _intToIp(int value) {
final int normalized = value & 0xFFFFFFFF;
return <int>[
(normalized >> 24) & 0xFF,
(normalized >> 16) & 0xFF,
(normalized >> 8) & 0xFF,
normalized & 0xFF,
];
}
}
lib/cidr_home.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'ad_banner_widget.dart';
import 'ad_manager.dart';
import 'cidr_calculator.dart';
import 'const_value.dart';
import 'l10n/app_localizations.dart';
import 'setting_page.dart';
class CidrHome extends StatefulWidget {
const CidrHome({
super.key,
required this.themeNumber,
required this.localeLanguage,
required this.onUpdateApp,
required this.prefs,
});
final int themeNumber; // 0 system, 1 light, 2 dark
final String localeLanguage; // '' system, 'en', 'ja'
final void Function({required int themeNumber, required String localeLanguage}) onUpdateApp;
final SharedPreferences prefs;
@override
State<CidrHome> createState() => _CidrHomeState();
}
class _CidrHomeState extends State<CidrHome> {
late final TextEditingController _ipController;
late final AdManager _adManager;
late int _selectedPrefix; // CIDR prefix length 1..32
late CidrCalculationResult _result;
late int _themeNumber;
late String _localeLanguage;
@override
void initState() {
super.initState();
_ipController = TextEditingController();
_ipController.addListener(_recalculate);
_selectedPrefix = 32;
_adManager = AdManager();
_themeNumber = <int>{0, 1, 2}.contains(widget.themeNumber) ? widget.themeNumber : 0;
_localeLanguage = widget.localeLanguage;
_result = CidrCalculator.calculate('', _selectedPrefix);
}
@override
void didUpdateWidget(covariant CidrHome oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.themeNumber != widget.themeNumber) {
_themeNumber = <int>{0, 1, 2}.contains(widget.themeNumber) ? widget.themeNumber : 0;
}
if (oldWidget.localeLanguage != widget.localeLanguage) {
_localeLanguage = widget.localeLanguage;
}
}
@override
void dispose() {
_ipController.removeListener(_recalculate);
_ipController.dispose();
_adManager.dispose();
super.dispose();
}
void _recalculate() {
if (!mounted) {
return;
}
setState(() {
_result = CidrCalculator.calculate(_ipController.text, _selectedPrefix);
});
}
void _onPrefixChanged(int? prefix) {
if (prefix == null || prefix == _selectedPrefix) {
return;
}
setState(() {
_selectedPrefix = prefix;
_result = CidrCalculator.calculate(_ipController.text, _selectedPrefix);
});
}
Future<void> _openSettings() async {
final Map<String, dynamic>? result = await Navigator.of(context).push<Map<String, dynamic>>(
MaterialPageRoute<Map<String, dynamic>>(
builder: (BuildContext context) => SettingPage(
themeNumber: _themeNumber,
localeLanguage: _localeLanguage,
),
),
);
if (result == null) {
return;
}
final int newThemeNumberRaw = (result[ConstValue.themeNumber] as int?) ?? _themeNumber;
final int newThemeNumber = <int>{0, 1, 2}.contains(newThemeNumberRaw) ? newThemeNumberRaw : 0;
final String newLocaleLanguage = (result[ConstValue.localeLanguage] as String?) ?? _localeLanguage;
if (newThemeNumber == _themeNumber && newLocaleLanguage == _localeLanguage) {
return;
}
await widget.prefs.setInt(ConstValue.themeNumber, newThemeNumber);
await widget.prefs.setString(ConstValue.localeLanguage, newLocaleLanguage);
setState(() {
_themeNumber = newThemeNumber;
_localeLanguage = newLocaleLanguage;
});
widget.onUpdateApp(themeNumber: newThemeNumber, localeLanguage: newLocaleLanguage);
}
@override
Widget build(BuildContext context) {
final AppLocalizations l = AppLocalizations.of(context);
final Locale locale = Localizations.localeOf(context);
final TextTheme textTheme = Theme.of(context).textTheme;
final ColorScheme colors = Theme.of(context).colorScheme;
final String ipDisplay = _result.sanitizedIp;
final String networkDisplay = CidrCalculator.formatIp(_result.networkParts);
final String broadcastDisplay = CidrCalculator.formatIp(_result.broadcastParts);
final String maskDisplay = CidrCalculator.formatIp(_result.maskParts);
final String cidrDisplay = '$networkDisplay/${_result.prefix}';
final String rangeDisplay = '$networkDisplay - $broadcastDisplay';
final String hostRangeDisplay = _formatHostRange(l, _result);
final String ipBitsDisplay = CidrCalculator.formatBits(_result.ipParts);
final String maskBitsDisplay = CidrCalculator.formatBits(_result.maskParts);
return Scaffold(
backgroundColor: colors.brightness == Brightness.light ? ConstValue.bgColors.back1.light : ConstValue.bgColors.back1.dark,
body: GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: SafeArea(
child: Column(
children: <Widget>[
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_buildHeader(l, textTheme, colors),
Container(
color: colors.brightness == Brightness.light ? ConstValue.bgColors.back2.light : ConstValue.bgColors.back2.dark,
child: _buildInputIp(l, locale, textTheme),
),
Container(
color: colors.brightness == Brightness.light ? ConstValue.bgColors.back3.light : ConstValue.bgColors.back3.dark,
child: _buildInputMask(l, locale, textTheme)
),
Container(
color: colors.brightness == Brightness.light ? ConstValue.bgColors.back4.light : ConstValue.bgColors.back4.dark,
child: _buildResult(l.labelIp, ipDisplay, textTheme, colors),
),
Container(
color: colors.brightness == Brightness.light ? ConstValue.bgColors.back5.light : ConstValue.bgColors.back5.dark,
child: _buildResult(l.labelRange, rangeDisplay, textTheme, colors),
),
Container(
color: colors.brightness == Brightness.light ? ConstValue.bgColors.back6.light : ConstValue.bgColors.back6.dark,
child: _buildResult(l.labelNetwork, networkDisplay, textTheme, colors),
),
Container(
color: colors.brightness == Brightness.light ? ConstValue.bgColors.back7.light : ConstValue.bgColors.back7.dark,
child: _buildResult(l.labelHostRange, hostRangeDisplay, textTheme, colors),
),
Container(
color: colors.brightness == Brightness.light ? ConstValue.bgColors.back8.light : ConstValue.bgColors.back8.dark,
child: _buildResult(l.labelBroadcast, broadcastDisplay, textTheme, colors),
),
Container(
color: colors.brightness == Brightness.light ? ConstValue.bgColors.back9.light : ConstValue.bgColors.back9.dark,
child: _buildResult(l.labelCidr, cidrDisplay, textTheme, colors),
),
Container(
color: colors.brightness == Brightness.light ? ConstValue.bgColors.back10.light : ConstValue.bgColors.back10.dark,
child: _buildResult(l.labelMask, maskDisplay, textTheme, colors),
),
Container(
color: colors.brightness == Brightness.light ? ConstValue.bgColors.back11.light : ConstValue.bgColors.back11.dark,
child: _buildBits(l.labelBits, ipBitsDisplay, maskBitsDisplay, textTheme, colors),
),
],
),
),
),
],
),
),
),
bottomNavigationBar: AdBannerWidget(adManager: _adManager),
);
}
Widget _buildHeader(AppLocalizations l, TextTheme textTheme, ColorScheme colors) {
return Row(children: [
Padding(
padding: const EdgeInsets.only(left: 10),
child: Text(l.appTitle,style: textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold, color: Colors.white54))
),
const Spacer(),
Padding(
padding: const EdgeInsets.only(right: 8),
child: IconButton(
onPressed: _openSettings,
tooltip: l.setting,
icon: Icon(Icons.settings, color: Colors.white54),
),
),
]);
}
Widget _buildInputIp(AppLocalizations l, Locale locale, TextTheme textTheme) {
final ColorScheme colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Column(crossAxisAlignment: CrossAxisAlignment.start,children: [
Row(
children: [
Text(l.inputIp,style: textTheme.bodySmall),
const SizedBox(width: 12),
Expanded(
child: TextField(
controller: _ipController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
style: textTheme.bodyMedium?.copyWith(fontFamily: 'monospace',fontWeight: FontWeight.w600),
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.allow(RegExp(r'[0-9.]')),
],
decoration: InputDecoration(
hintText: '0.0.0.0',
filled: true,
fillColor: Colors.transparent,
),
),
),
],
),
]),
],
)
);
}
Widget _buildInputMask(AppLocalizations l, Locale locale, TextTheme textTheme) {
final ColorScheme colors = Theme.of(context).colorScheme;
final NumberFormat numberFormat = NumberFormat.decimalPattern(locale.languageCode);
final List<DropdownMenuItem<int>> items = List<DropdownMenuItem<int>>.generate(32, (int index) {
final int prefix = 32 - index;
final String optionLabel = _formatSubnetOption(prefix, numberFormat);
return DropdownMenuItem<int>(
value: prefix,
child: Text(optionLabel, style: textTheme.bodyMedium),
);
});
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Column(crossAxisAlignment: CrossAxisAlignment.start,children: [
Text(l.inputSubnet, style: textTheme.bodySmall),
InputDecorator(
decoration: InputDecoration(
filled: true,
fillColor: Colors.transparent,
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<int>(
value: _selectedPrefix,
isExpanded: true,
items: items,
onChanged: _onPrefixChanged,
style: textTheme.bodyMedium?.copyWith(fontFamily: 'monospace',fontWeight: FontWeight.w600),
),
),
),
])
],
)
);
}
Widget _buildResult(String label, String value, TextTheme textTheme, ColorScheme colors) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Expanded(
flex: 4,
child: Text(label,style: textTheme.bodySmall),
),
Expanded(
flex: 7,
child: Text(value,style: textTheme.bodyMedium?.copyWith(fontFamily: 'monospace',fontWeight: FontWeight.w600)),
),
],
),
);
}
Widget _buildBits(String label, String ipBits, String maskBits, TextTheme textTheme, ColorScheme colors) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
child: SizedBox(
width: double.infinity, // 横幅100%
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
label,
style: textTheme.bodySmall,
),
const SizedBox(height: 2),
Text(
ipBits,
style: textTheme.bodyMedium?.copyWith(fontFamily: 'monospace',fontWeight: FontWeight.w600),
),
Text(
maskBits,
style: textTheme.bodyMedium?.copyWith(fontFamily: 'monospace',fontWeight: FontWeight.w600),
),
],
),
),
);
}
String _formatSubnetOption(int prefix, NumberFormat numberFormat) {
final List<int> maskParts = CidrCalculator.maskFromPrefix(prefix);
final String mask = CidrCalculator.formatIp(maskParts);
final int hostCount = CidrCalculator.hostCountForPrefix(prefix);
final String hostCountString = numberFormat.format(hostCount);
return '$mask (/$prefix) [$hostCountString]';
}
String _formatHostRange(AppLocalizations l, CidrCalculationResult result) {
final String network = CidrCalculator.formatIp(result.networkParts);
final String broadcast = CidrCalculator.formatIp(result.broadcastParts);
if (result.hostCount <= 1) {
return '$network ${l.hostSingleSuffix}';
}
if (result.hostCount == 2) {
return '$network - $broadcast ${l.hostTwoSuffix}';
}
final List<int> firstHost = CidrCalculator.intToIp(result.networkInt + 1);
final List<int> lastHost = CidrCalculator.intToIp(result.broadcastInt - 1);
return '${CidrCalculator.formatIp(firstHost)} - ${CidrCalculator.formatIp(lastHost)}';
}
}
lib/const_value.dart
import 'package:flutter/material.dart';
class ConstValue {
static const String settings = 'settings';
static const String localeLanguage = 'localeLanguage';
static const String themeNumber = 'themeNumber'; // 0 system, 1 light, 2 dark
static const BgColors bgColors = BgColors();
}
class BgColors {
const BgColors();
final BackColor back1 = const BackColor(
light: Color(0xFFc19fd8),
dark: Color(0xFF5d3e73),
);
final BackColor back2 = const BackColor(
light: Color(0xFFaa9fd8),
dark: Color(0xFF483d73),
);
final BackColor back3 = const BackColor(
light: Color(0xFFa0abd8),
dark: Color(0xFF3e4873),
);
final BackColor back4 = const BackColor(
light: Color(0xFF9fbcd9),
dark: Color(0xFF3d5873),
);
final BackColor back5 = const BackColor(
light: Color(0xFF9ecfdb),
dark: Color(0xFF3d6a75),
);
final BackColor back6 = const BackColor(
light: Color(0xFF9edcce),
dark: Color(0xFF3c7669),
);
final BackColor back7 = const BackColor(
light: Color(0xFF9edcad),
dark: Color(0xFF3c764b),
);
final BackColor back8 = const BackColor(
light: Color(0xFFb5dca4),
dark: Color(0xFF527642),
);
final BackColor back9 = const BackColor(
light: Color(0xFFd8dca4),
dark: Color(0xFF727643),
);
final BackColor back10 = const BackColor(
light: Color(0xFFdbc3a4),
dark: Color(0xFF765e42),
);
final BackColor back11 = const BackColor(
light: Color(0xFFdbafa2),
dark: Color(0xFF764b40),
);
final BackColor back12 = const BackColor(
light: Color(0xFFdba0b0),
dark: Color(0xFF763e4d),
);
final BackColor back13 = const BackColor(
light: Color(0xFFdba0cc),
dark: Color(0xFF763e68),
);
}
class BackColor {
final Color light;
final Color dark;
const BackColor({required this.light, required this.dark});
}
lib/main.dart
import 'package:flutter/material.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'cidr_home.dart';
import 'const_value.dart';
import 'l10n/app_localizations.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await MobileAds.instance.initialize();
final SharedPreferences prefs = await SharedPreferences.getInstance();
final int initialThemeNumber = prefs.getInt(ConstValue.themeNumber) ?? 0;
final String initialLocaleLanguage = prefs.getString(ConstValue.localeLanguage) ?? '';
runApp(CidrApp(
prefs: prefs,
initialThemeNumber: initialThemeNumber,
initialLocaleLanguage: initialLocaleLanguage,
));
}
class CidrApp extends StatefulWidget {
const CidrApp({
super.key,
required this.prefs,
required this.initialThemeNumber,
required this.initialLocaleLanguage,
});
final SharedPreferences prefs;
final int initialThemeNumber;
final String initialLocaleLanguage;
@override
State<CidrApp> createState() => _CidrAppState();
}
class _CidrAppState extends State<CidrApp> {
late int _themeNumber; // 0 system, 1 light, 2 dark
late String _localeLanguage; // '' system default, otherwise language code
@override
void initState() {
super.initState();
_themeNumber = _normalizeThemeNumber(widget.initialThemeNumber);
_localeLanguage = widget.initialLocaleLanguage;
}
void _updateThemeAndLocale({required int themeNumber, required String localeLanguage}) {
setState(() {
_themeNumber = _normalizeThemeNumber(themeNumber);
_localeLanguage = localeLanguage;
});
}
int _normalizeThemeNumber(int value) {
if (value < 0 || value > 2) {
return 0;
}
return value;
}
ThemeMode get _themeMode {
switch (_themeNumber) {
case 2:
return ThemeMode.dark;
case 1:
return ThemeMode.light;
default:
return ThemeMode.system;
}
}
Locale? get _locale => _localeLanguage.isEmpty ? null : Locale(_localeLanguage);
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'CIDR',
themeMode: _themeMode,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blueGrey),
useMaterial3: true,
),
darkTheme: ThemeData.dark(useMaterial3: true),
locale: _locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: CidrHome(
themeNumber: _themeNumber,
localeLanguage: _localeLanguage,
onUpdateApp: _updateThemeAndLocale,
prefs: widget.prefs,
),
);
}
}
lib/setting_page.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'ad_banner_widget.dart';
import 'ad_manager.dart';
import 'ad_ump_status.dart';
import 'const_value.dart';
import 'l10n/app_localizations.dart';
class SettingPage extends StatefulWidget {
const SettingPage({
super.key,
required this.themeNumber,
required this.localeLanguage,
});
final int themeNumber; // 0 system, 1 light, 2 dark
final String localeLanguage; // '' system default, otherwise language code
@override
State<SettingPage> createState() => _SettingPageState();
}
class _SettingPageState extends State<SettingPage> {
static const Map<String, String> _languageOptions = <String, String>{
'en': 'English',
'ja': 'Japanese',
};
late int _themeNumber; // 0 system, 1 light, 2 dark
late String _languageCode;
late AdManager _adManager;
late final UmpConsentController _adUmp;
AdUmpState _adUmpState = AdUmpState.initial;
@override
void initState() {
super.initState();
_themeNumber =
<int>{0, 1, 2}.contains(widget.themeNumber) ? widget.themeNumber : 0;
_languageCode = widget.localeLanguage;
if (_languageCode.isNotEmpty &&
!_languageOptions.containsKey(_languageCode)) {
_languageCode = '';
}
_adManager = AdManager();
_adUmp = UmpConsentController();
unawaited(_refreshConsentInfo());
}
@override
void dispose() {
_adManager.dispose();
super.dispose();
}
void _onApply() {
Navigator.of(context).pop(<String, Object?>{
ConstValue.themeNumber: _themeNumber,
ConstValue.localeLanguage: _languageCode,
});
}
Future<void> _refreshConsentInfo() async {
if (!mounted) {
return;
}
setState(() {
_adUmpState = _adUmpState.copyWith(isChecking: true);
});
final AdUmpState state = await _adUmp.updateConsentInfo(
current: _adUmpState,
);
if (!mounted) {
return;
}
setState(() {
_adUmpState = state;
});
}
Future<void> _onTapPrivacyOptions() async {
final FormError? error = await _adUmp.showPrivacyOptions();
await _refreshConsentInfo();
if (error != null && mounted) {
final AppLocalizations l = AppLocalizations.of(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${l.cmpErrorOpeningSettings} ${error.message}'),
),
);
}
}
@override
Widget build(BuildContext context) {
final AppLocalizations l = AppLocalizations.of(context);
final ThemeData theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
leading: IconButton(
icon: const Icon(Icons.close),
tooltip: l.cancel,
onPressed: () => Navigator.of(context).pop(),
),
actions: <Widget>[
Padding(
padding: const EdgeInsets.only(right: 10),
child: IconButton(
icon: const Icon(Icons.check),
tooltip: l.apply,
onPressed: _onApply,
),
),
],
),
body: SafeArea(
child: GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
child: Column(
children: <Widget>[
_buildThemeCard(l, theme),
const SizedBox(height: 12),
_buildLanguageCard(l, theme),
const SizedBox(height: 12),
_buildPrivacyCard(l, theme),
const SizedBox(height: 120),
],
),
),
),
),
bottomNavigationBar: AdBannerWidget(adManager: _adManager),
);
}
Widget _buildThemeCard(AppLocalizations l, ThemeData theme) {
final Color underlineColor = theme.colorScheme.outlineVariant;
return Card(
margin: EdgeInsets.zero,
elevation: 0,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
clipBehavior: Clip.antiAlias,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 20, 16, 24),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Expanded(
child: Text(l.theme, style: theme.textTheme.titleMedium),
),
const SizedBox(width: 16),
Expanded(
child: DropdownButton<int>(
value: _themeNumber,
isExpanded: true,
underline: Container(
height: 1,
color: underlineColor,
),
items: <DropdownMenuItem<int>>[
DropdownMenuItem<int>(value: 0, child: Text(l.systemDefault)),
DropdownMenuItem<int>(value: 1, child: Text(l.lightTheme)),
DropdownMenuItem<int>(value: 2, child: Text(l.darkTheme)),
],
onChanged: (int? value) {
if (value == null || value == _themeNumber) {
return;
}
setState(() => _themeNumber = value);
},
),
),
],
),
),
);
}
Widget _buildLanguageCard(AppLocalizations l, ThemeData theme) {
final String? dropdownValue = _languageCode.isEmpty ? null : _languageCode;
final Color underlineColor = theme.colorScheme.outlineVariant;
return Card(
margin: EdgeInsets.zero,
elevation: 0,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
clipBehavior: Clip.antiAlias,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 20, 16, 24),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Expanded(
child: Text(l.language, style: theme.textTheme.titleMedium),
),
const SizedBox(width: 16),
Expanded(
child: DropdownButton<String?>(
value: dropdownValue,
isExpanded: true,
hint: Text(l.systemDefault),
underline: Container(
height: 1,
color: underlineColor,
),
items: <DropdownMenuItem<String?>>[
DropdownMenuItem<String?>(
value: null,
child: Text(l.systemDefault),
),
..._languageOptions.entries.map(
(MapEntry<String, String> entry) =>
DropdownMenuItem<String?>(
value: entry.key,
child: Text(entry.value),
),
),
],
onChanged: (String? value) {
setState(() => _languageCode = value ?? "");
},
),
),
],
),
),
);
}
Widget _buildPrivacyCard(AppLocalizations l, ThemeData theme) {
String statusLabel = l.cmpCheckingRegion;
IconData statusIcon = Icons.help_outline;
switch (_adUmpState.privacyStatus) {
case PrivacyOptionsRequirementStatus.required:
statusLabel = l.cmpRegionRequiresSettings;
statusIcon = Icons.privacy_tip;
break;
case PrivacyOptionsRequirementStatus.notRequired:
statusLabel = l.cmpRegionNoSettingsRequired;
statusIcon = Icons.check_circle_outline;
break;
case PrivacyOptionsRequirementStatus.unknown:
statusLabel = l.cmpRegionCheckFailed;
statusIcon = Icons.error_outline;
break;
}
final bool showButtons =
_adUmpState.privacyStatus == PrivacyOptionsRequirementStatus.required;
return Card(
margin: EdgeInsets.zero,
elevation: 0,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
clipBehavior: Clip.antiAlias,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 20, 16, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(l.cmpSettingsTitle, style: theme.textTheme.titleMedium),
const SizedBox(height: 8),
Text(l.cmpConsentDescription, style: theme.textTheme.bodySmall),
const SizedBox(height: 16),
Center(
child: Column(
children: <Widget>[
Chip(
avatar: Icon(statusIcon, size: 18),
label: Text(statusLabel),
side: BorderSide.none,
),
const SizedBox(height: 4),
Text(
'${l.cmpConsentStatusLabel} ${_adUmpState.consentStatus.localized(context)}',
style: theme.textTheme.bodySmall,
textAlign: TextAlign.center,
),
if (showButtons)
Column(
children: <Widget>[
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _adUmpState.isChecking
? null
: _onTapPrivacyOptions,
icon: const Icon(Icons.settings),
label: Text(
_adUmpState.isChecking
? l.cmpConsentStatusChecking
: l.cmpOpenConsentSettings,
),
style: ElevatedButton.styleFrom(
elevation: 0,
side: const BorderSide(width: 1),
),
),
const SizedBox(height: 16),
OutlinedButton.icon(
onPressed: _adUmpState.isChecking
? null
: () => _refreshConsentInfo(),
icon: const Icon(Icons.refresh),
label: Text(l.cmpRefreshStatus),
),
const SizedBox(height: 16),
OutlinedButton.icon(
onPressed: _adUmpState.isChecking
? null
: () async {
await ConsentInformation.instance.reset();
await _refreshConsentInfo();
if (!mounted) {
return;
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l.cmpResetStatusDone),
),
);
},
icon: const Icon(Icons.restore),
label: Text(l.cmpResetStatus),
),
],
),
],
),
),
],
),
),
);
}
}