pubspec.yaml
name: lunarage
description: "Lunar Age"
# 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: 1.0.8+9
environment:
sdk: ^3.6.0-198.0.dev
# 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
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
webview_flutter: ^4.10.0
webview_flutter_android: any
webview_flutter_wkwebview: any
google_mobile_ads: ^6.0.0
package_info_plus: ^9.0.0
path_provider: ^2.0.11
dev_dependencies:
flutter_test:
sdk: flutter
flutter_launcher_icons: ^0.14.3 #flutter pub run flutter_launcher_icons
flutter_native_splash: ^2.4.4 #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
flutter_launcher_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: '#000000'
image: 'assets/image/splash.png'
color_dark: '#000000'
image_dark: 'assets/image/splash.png'
fullscreen: true
android_12:
icon_background_color: '#000000'
image: 'assets/image/splash.png'
icon_background_color_dark: '#000000'
image_dark: 'assets/image/splash.png'
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# 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
assets:
- assets/icon/
- assets/image/
- assets/httpdocs/
- assets/httpdocs/common/css/
- assets/httpdocs/image/
- assets/httpdocs/js/
# 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:lunarage/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 Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 10),
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/const_value.dart
///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-10-15
///
library;
import 'package:flutter/material.dart';
class ConstValue {
//color
static const Color colorHeader = Color.fromRGBO(0,0,0,0.1);
static const Color colorText = Color.fromRGBO(255,255,255,1);
static const Color colorBack = Color.fromRGBO(0,0,0,1);
static const Color colorSettingHeader = Color.fromRGBO(69, 99, 202, 1.0);
static const Color colorUiActiveColor = Color.fromRGBO(69, 99, 202, 1.0);
static const Color colorUiInactiveColor = Colors.black26;
}
lib/local_server.dart
///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2025-02-20
///
library;
import 'dart:async';
import 'dart:io';
import 'package:flutter/services.dart';
class LocalServer {
late HttpServer _server;
String _serverUrl = '';
final Map<String, Uint8List> _cache = {};
final Map<String, ContentType> _contentTypeCache = {};
LocalServer() { //constructor
}
Future<void> start() async {
final Completer<void> completer = Completer<void>();
_server = await HttpServer.bind(InternetAddress.loopbackIPv4, 8080);
_serverUrl = 'http://${_server.address.host}:${_server.port}/';
_server.listen((HttpRequest request) async {
final pathSegments = request.uri.pathSegments;
final String rawPath = pathSegments.join('/');
final String pathWithoutQuery = rawPath.split('?')[0];
final String path = pathWithoutQuery.isEmpty ? 'assets/httpdocs/index.html' : 'assets/httpdocs/$pathWithoutQuery';
try {
// Check cache first
if (_cache.containsKey(path)) {
request.response.headers.contentType = _contentTypeCache[path];
request.response.headers.set(HttpHeaders.cacheControlHeader, 'public, max-age=31536000'); // Cache for 1 year
request.response.add(_cache[path]!);
await request.response.close();
return;
}
ContentType contentType;
if (path.endsWith('.html')) {
contentType = ContentType.html;
} else if (path.endsWith('.css')) {
contentType = ContentType('text', 'css');
} else if (path.endsWith('.js')) {
contentType = ContentType('application', 'javascript');
} else if (path.endsWith('.webp')) {
contentType = ContentType('image', 'webp');
} else if (path.endsWith('.ico')) {
contentType = ContentType('image', 'icon');
} else {
contentType = ContentType('application', 'octet-stream');
}
final ByteData data = await rootBundle.load(path);
final Uint8List bytes = data.buffer.asUint8List();
// Store in cache
_cache[path] = bytes;
_contentTypeCache[path] = contentType;
request.response.headers.contentType = contentType;
request.response.headers.set(HttpHeaders.cacheControlHeader, 'public, max-age=31536000'); // Cache for 1 year
request.response.add(bytes);
await request.response.close();
} catch (e) {
request.response.statusCode = HttpStatus.notFound;
request.response.write('404 Not Found');
await request.response.close();
}
});
completer.complete();
return completer.future;
}
void close() {
_server.close();
}
String url() {
return _serverUrl;
}
}
lib/main.dart
///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2025-02-17
///
library;
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:lunarage/const_value.dart';
import 'package:lunarage/version_state.dart';
import 'package:lunarage/setting.dart';
import 'package:lunarage/ad_manager.dart';
import 'package:lunarage/ad_banner_widget.dart';
import 'package:lunarage/my_web_view_controller.dart';
import 'package:lunarage/local_server.dart';
void main() {
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MainHomePage(),
);
}
}
class MainHomePage extends StatefulWidget {
const MainHomePage({super.key});
@override
State<MainHomePage> createState() => _MainHomePageState();
}
class _MainHomePageState extends State<MainHomePage> {
late AdManager _adManager;
final MyWebViewController _myWebViewController = MyWebViewController(); //WebViewController
final LocalServer _localServer = LocalServer(); //ローカルサーバー
late final WebViewController _webViewController;
late final List<int> _yearList;
late final List<DropdownMenuItem<int>> _yearDropdownItems;
final List<int> _monthList = List<int>.generate(12, (index) => index + 1);
late final List<DropdownMenuItem<int>> _monthDropdownItems;
int _selectedYear = DateTime.now().year;
int _selectedMonth = DateTime.now().month;
//アプリのバージョン取得
void _getVersion() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
setState(() {
VersionState.versionSave(packageInfo.version);
});
}
void _updateWebView() async {
String serverUrl = _localServer.url();
await _webViewController.loadRequest(Uri.parse('${serverUrl}index.html?year=$_selectedYear&month=$_selectedMonth'));
}
List<int> _generateYearList() {
int currentYear = DateTime.now().year;
return List<int>.generate(161, (index) => currentYear - 80 + index);
}
void _incrementYear() {
setState(() {
if (_selectedYear < _yearList.last) {
_selectedYear += 1;
}
_updateWebView();
});
}
void _decrementYear() {
setState(() {
if (_selectedYear > _yearList.first) {
_selectedYear -= 1;
}
_updateWebView();
});
}
void _incrementMonth() {
setState(() {
if (_selectedMonth < 12) {
_selectedMonth += 1;
} else {
if (_selectedYear < _yearList.last) {
_selectedMonth = 1;
_selectedYear += 1;
}
}
_updateWebView();
});
}
void _decrementMonth() {
setState(() {
if (_selectedMonth > 1) {
_selectedMonth -= 1;
} else {
if (_selectedYear > _yearList.first) {
_selectedMonth = 12;
_selectedYear -= 1;
}
}
_updateWebView();
});
}
//ページ起動開始時に一度だけ呼ばれる
@override
void initState() {
super.initState();
_adManager = AdManager();
_yearList = _generateYearList();
_yearDropdownItems = _yearList.map<DropdownMenuItem<int>>((int year) {
return DropdownMenuItem<int>(
value: year,
child: Text(year.toString(),
style: const TextStyle(
color: ConstValue.colorText,
)
),
);
}).toList();
_monthDropdownItems = _monthList.map<DropdownMenuItem<int>>((int month) {
return DropdownMenuItem<int>(
value: month,
child: Text(
month.toString(),
style: const TextStyle(color: Colors.white), // テキストの色を設定
),
);
}).toList();
_webViewController = _myWebViewController.controller();
_initApp();
}
// アプリの非同期初期化処理
Future<void> _initApp() async {
_getVersion();
// ローカルサーバーが起動してからWebViewをロードする
await _localServer.start();
_updateWebView();
}
//ページ終了時に一度だけ呼ばれる
@override
void dispose() {
_adManager.dispose();
_localServer.close();
super.dispose();
}
//画面全体
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: ConstValue.colorHeader,
//タイトル表示
title: const Text('Lunar age',
style: TextStyle(
color: Colors.white,
fontSize: 15.0,
)
),
//設定ボタン
actions: <Widget>[
TextButton(
onPressed: () async {
bool? ret = await Navigator.of(context).push(
MaterialPageRoute<bool>(builder:(context) => const SettingPage()),
);
//awaitで呼び出しているので、settingから戻ったら以下が実行される。
if (ret!) { //設定で適用だった場合
setState(() {});
}
},
child: Text('Information',
style: const TextStyle(
color: Colors.white,
)
)
)
]
),
body: SafeArea(
child: Column(children:[
Row(children:[
Expanded(child: _selectYear()),
Expanded(child: _selectMonth()),
]),
Expanded(
child: Padding(
padding: const EdgeInsets.only(top: 10, left: 10, right: 10, bottom: 0),
child: Center(
child: WebViewWidget(controller: _webViewController),
),
)
),
])
),
bottomNavigationBar: AdBannerWidget(adManager: _adManager),
);
}
Widget _selectYear() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(Icons.chevron_left),
color: Colors.white,
onPressed: _decrementYear,
),
DropdownButton<int>(
value: _selectedYear,
onChanged: (int? newValue) {
setState(() {
_selectedYear = newValue!;
_updateWebView();
});
},
dropdownColor: Colors.grey[850],
items: _yearDropdownItems,
),
IconButton(
icon: Icon(Icons.chevron_right),
color: Colors.white,
onPressed: _incrementYear,
),
],
);
}
Widget _selectMonth() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(Icons.chevron_left),
color: Colors.white,
onPressed: _decrementMonth,
),
DropdownButton<int>(
value: _selectedMonth,
onChanged: (int? newValue) {
setState(() {
_selectedMonth = newValue!;
_updateWebView();
});
},
dropdownColor: Colors.grey[850], // ドロップダウンの背景色を設定
items: _monthDropdownItems,
),
IconButton(
icon: Icon(Icons.chevron_right),
color: Colors.white,
onPressed: _incrementMonth,
),
],
);
}
}
lib/my_web_view_controller.dart
///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2025-02-20
///
library;
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:webview_flutter_android/webview_flutter_android.dart';
import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';
class MyWebViewController {
MyWebViewController() {
//constructor
}
WebViewController controller() {
late final PlatformWebViewControllerCreationParams params;
if (WebViewPlatform.instance is WebKitWebViewPlatform) {
params = WebKitWebViewControllerCreationParams(
allowsInlineMediaPlayback: true,
mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
);
} else {
params = const PlatformWebViewControllerCreationParams();
}
final WebViewController controller = WebViewController.fromPlatformCreationParams(params);
controller
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onUrlChange: (UrlChange change) {
//debugPrint('url change to ${change.url}');
},
),
);
controller.setBackgroundColor(const Color(0x00000000));
if (controller.platform is AndroidWebViewController) {
AndroidWebViewController.enableDebugging(true);
(controller.platform as AndroidWebViewController).setMediaPlaybackRequiresUserGesture(false);
}
return controller;
}
}
lib/setting.dart
///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-10-15
///
library;
import 'package:flutter/material.dart';
import 'package:lunarage/version_state.dart';
import 'package:lunarage/ad_manager.dart';
import 'package:lunarage/ad_banner_widget.dart';
class SettingPage extends StatefulWidget {
const SettingPage({super.key});
@override
State<SettingPage> createState() => _SettingPageState();
}
class _SettingPageState extends State<SettingPage> {
late AdManager _adManager;
//ページ起動時に一度だけ実行される
@override
void initState() {
super.initState();
_adManager = AdManager();
}
//ページ終了時に一度だけ実行される
@override
void dispose() {
_adManager.dispose();
super.dispose();
}
//ページ描画
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
appBar: AppBar(
centerTitle: true,
elevation: 0,
//設定キャンセルボタン
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
Navigator.of(context).pop(false); //falseを返す
},
),
title: Text(
'Information',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
foregroundColor: Theme.of(context).colorScheme.onSurface,
actions: [
//設定OKボタン
IconButton(
icon: const Icon(Icons.check),
onPressed: () async {
if (!mounted) {
return;
}
Navigator.of(context).pop(true); //trueを返す
},
),
],
),
body: SafeArea(
child: Column(children:[
Expanded(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(children: [
Padding(
padding: const EdgeInsets.only(top: 24, left: 0, right: 0, bottom: 36),
child: SizedBox(
child: Text('version ${VersionState.versionLoad()}',
style: TextStyle(
fontSize: 10,
color: Theme.of(context).colorScheme.onSurface,
),
),
),
),
]),
),
),
]),
),
bottomNavigationBar: AdBannerWidget(adManager: _adManager),
);
}
}
lib/version_state.dart
///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-01-27
///
library;
class VersionState {
static String _version = '';
//バージョンを記録
static void versionSave(String str) {
_version = str;
}
//バージョンを返す
static String versionLoad() {
return _version;
}
}