ソースコード source code

下記アプリの主要なソースコードを公開しています。アプリ開発の参考になれば幸いです。

画像等が別途必要ですので下記情報のみでアプリが完成するものではありません。 アプリは少しずつ機能拡張していますのでストア公開されているアプリと内容が異なる場合があります。 コードはコピーして自由にお使いいただけます。ただし著作権は放棄しておりませんので全部の再掲載はご遠慮ください。部分的に再掲載したり、改変して再掲載するのは構いません。 自身のアプリ作成の参考として個人使用・商用問わず自由にお使いいただけます。 コード記述のお手本を示すものではありません。ミニアプリですので変数名などさほど気遣いしていない部分も有りますし間違いも有るかと思いますので参考程度にお考え下さい。 他の賢者の皆様が公開されているコードを参考にした箇所も含まれます。Flutter開発の熟練者が書いたコードではありません。 エンジニア向け技術情報共有サービスではありませんので説明は省いています。 GitHubなどへの公開は予定しておりません。

下記コードの最終ビルド日: 2025-09-20

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.0+19

environment:
  sdk: ">=3.3.0 <4.0.0"

# 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:roulettewheeleurope/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

import 'dart:async';
import 'dart:io' show Platform;
import 'dart:ui';
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;

  Future<void> loadAdaptiveBannerAd(int widthPx, VoidCallback onAdLoaded) async {
    _onLoadedCb = onAdLoaded;
    _lastWidthPx = widthPx;
    _retryAttempt = 0;
    _retryTimer?.cancel();
    _startLoad(widthPx);
  }

  Future<void> _startLoad(int widthPx) async {
    _bannerAd?.dispose();

    AnchoredAdaptiveBannerAdSize? adaptiveSize;
    try {
      adaptiveSize = await AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize(widthPx);
    } catch (_) {
      adaptiveSize = null;
    }
    final AdSize size = adaptiveSize ?? AdSize.fullBanner;

    _bannerAd = BannerAd(
      adUnitId: _adUnitId,
      request: const 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() {
    _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();
  }

}

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 'package:flutter/material.dart';

import 'ad_banner_widget.dart';
import 'ad_manager.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': '日本語',
  };

  late int _themeNumber; // 0 system, 1 light, 2 dark
  late String _languageCode;
  late AdManager _adManager;

  @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();
  }

  @override
  void dispose() {
    _adManager.dispose();
    super.dispose();
  }

  void _onApply() {
    Navigator.of(context).pop(<String, Object?>{
      ConstValue.themeNumber: _themeNumber,
      ConstValue.localeLanguage: _languageCode,
    });
  }

  @override
  Widget build(BuildContext context) {
    final AppLocalizations l = AppLocalizations.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: ListView(
          padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
          children: <Widget>[
            _buildThemeTile(l),
            const Divider(),
            _buildLanguageTile(l),
            const SizedBox(height: 120),
          ],
        ),
      ),
      bottomNavigationBar: AdBannerWidget(adManager: _adManager),
    );
  }

  Widget _buildThemeTile(AppLocalizations l) {
    return ListTile(
      contentPadding: const EdgeInsets.only(left: 16, right: 10),
      title: Text(l.theme),
      trailing: DropdownButton<int>(
        value: _themeNumber,
        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) {
            return;
          }
          setState(() => _themeNumber = value);
        },
      ),
    );
  }

  Widget _buildLanguageTile(AppLocalizations l) {
    final String? dropdownValue = _languageCode.isEmpty ? null : _languageCode;
    return ListTile(
      contentPadding: const EdgeInsets.only(left: 16, right: 10),
      title: Text(l.language),
      trailing: DropdownButton<String?>(
        value: dropdownValue,
        hint: Text(l.systemDefault),
        items: <DropdownMenuItem<String?>>[
          DropdownMenuItem<String?>(value: null, child: Text(l.systemDefault))
        ]
            .followedBy(
              _languageOptions.entries.map(
                (MapEntry<String, String> entry) => DropdownMenuItem<String?>(
                  value: entry.key,
                  child: Text(entry.value),
                ),
              ),
            )
            .toList(),
        onChanged: (String? value) {
          setState(() => _languageCode = value ?? '');
        },
      ),
    );
  }
}