lib/scan_provider.dart
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class DuplicateRemovalEntry {
const DuplicateRemovalEntry({required this.data, required this.removedCount});
final String data;
final int removedCount;
}
class ScanProvider extends ChangeNotifier {
List<String> _scanResults = [];
static const String _scanResultsKey = 'scan_results';
List<String> get scanResults => _scanResults;
Future<void> loadScanResults() async {
final prefs = await SharedPreferences.getInstance();
_scanResults = prefs.getStringList(_scanResultsKey) ?? [];
notifyListeners();
}
Future<void> addScanResult(String result) async {
_scanResults.add(result);
await _saveResults();
}
Future<void> deleteScanResult(int index) async {
if (index >= 0 && index < _scanResults.length) {
_scanResults.removeAt(index);
await _saveResults();
}
}
Future<void> deleteLastScanResult() async {
if (_scanResults.isNotEmpty) {
_scanResults.removeLast();
await _saveResults();
}
}
Future<void> deleteAllScanResults() async {
_scanResults.clear();
await _saveResults();
}
Future<List<DuplicateRemovalEntry>> removeDuplicateScanResults() async {
final seenKeys = <String>{};
final removalCounts = <String, int>{};
final displayTexts = <String, String>{};
final indexesToRemove = <int>[];
for (var i = 0; i < _scanResults.length; i++) {
final entry = _scanResults[i];
final parts = entry.split(',');
final hasContent = parts.length >= 3;
final format = hasContent ? parts[1] : '';
final content = hasContent ? parts.sublist(2).join(',') : entry;
final key = hasContent ? '$format::$content' : entry;
displayTexts.putIfAbsent(key, () => content);
if (!seenKeys.add(key)) {
removalCounts.update(key, (value) => value + 1, ifAbsent: () => 1);
indexesToRemove.add(i);
}
}
if (indexesToRemove.isEmpty) {
return const <DuplicateRemovalEntry>[];
}
indexesToRemove.sort((a, b) => b.compareTo(a));
for (final index in indexesToRemove) {
_scanResults.removeAt(index);
}
await _saveResults();
return removalCounts.entries
.map(
(entry) => DuplicateRemovalEntry(
data: displayTexts[entry.key] ?? entry.key,
removedCount: entry.value,
),
)
.toList();
}
Future<void> _saveResults() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setStringList(_scanResultsKey, _scanResults);
notifyListeners();
}
String getFormattedResults() {
return _scanResults.map((res) {
final parts = res.split(',');
if (parts.length >= 2) {
parts[1] = parts[1].split('.').last;
return parts.join(',');
}
return res;
}).join('\n');
}
String getLastScanContent() {
if (_scanResults.isEmpty) return '';
final parts = _scanResults.last.split(',');
if (parts.length >= 3) {
return parts.sublist(2).join(',');
}
return _scanResults.last;
}
String toCsv() {
return _scanResults
.map((line) {
final parts = line.split(',');
if (parts.length >= 3) {
final timestamp = parts[0];
final format = parts[1];
final content =
'"${parts.sublist(2).join(',').replaceAll('"', '""')}"';
return '"$timestamp","$format",$content';
}
return line;
})
.join('\n');
}
}
lib/scanner_page.dart
import 'dart:async';
import 'package:audioplayers/audioplayers.dart';
import 'package:barcodereader/settings_provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
import 'l10n/app_localizations.dart';
class ScannerPage extends StatefulWidget {
final Function(String result) onScan;
const ScannerPage({super.key, required this.onScan});
@override
State<ScannerPage> createState() => _ScannerPageState();
}
class _ScannerPageState extends State<ScannerPage> {
final MobileScannerController _scannerController = MobileScannerController();
bool _isScanPaused = false;
bool _isFlashing = false;
@override
void initState() {
super.initState();
_setOrientation();
}
void _setOrientation() {
final settings = context.read<SettingsProvider>();
if (settings.lockOrientation) {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
}
}
@override
void dispose() {
_scannerController.dispose();
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
super.dispose();
}
@override
Widget build(BuildContext context) {
final settings = context.watch<SettingsProvider>();
return Scaffold(
appBar: AppBar(
backgroundColor: _isFlashing ? Colors.yellow : null,
foregroundColor: _isFlashing ? Colors.black : null,
actions: [
IconButton(
icon: ValueListenableBuilder<MobileScannerState>(
valueListenable: _scannerController,
builder: (context, state, child) {
switch (state.torchState) {
case TorchState.off:
return Icon(
Icons.flash_off,
color: _isFlashing ? Colors.grey : Colors.white,
);
case TorchState.on:
return const Icon(Icons.flash_on, color: Colors.yellow);
default:
return const Icon(Icons.no_flash, color: Colors.grey);
}
},
),
onPressed: () => _scannerController.toggleTorch(),
),
IconButton(
icon: const Icon(Icons.flip_camera_ios),
onPressed: () => _scannerController.switchCamera(),
),
],
),
body: MobileScanner(
controller: _scannerController,
onDetect: (capture) {
if (_isScanPaused) return;
final List<Barcode> barcodes = capture.barcodes;
if (barcodes.isNotEmpty) {
if (mounted) {
setState(() {
_isScanPaused = true;
});
}
final String code =
barcodes.first.rawValue ?? AppLocalizations.of(context)!.noData;
final String format = barcodes.first.format.name;
final String result =
'${DateFormat('yyyyMMdd_HHmmss').format(DateTime.now())},$format,$code';
widget.onScan(result);
if (settings.beepOnScan) {
final soundId = settings.selectedBeepSound;
AudioPlayer().play(AssetSource('sound/beep$soundId.mp3'));
}
if (settings.flashOnScan) {
if (mounted) {
setState(() {
_isFlashing = true;
});
}
}
if (settings.continuousScan) {
if (settings.flashOnScan) {
Timer(const Duration(milliseconds: 200), () {
if (mounted) {
setState(() {
_isFlashing = false;
});
}
});
}
final interval = (settings.scanInterval * 1000).toInt();
Timer(Duration(milliseconds: interval), () {
if (mounted) {
setState(() {
_isScanPaused = false;
});
}
});
} else {
final popDelay = settings.flashOnScan ? 200 : 50;
Timer(Duration(milliseconds: popDelay), () {
if (mounted) {
Navigator.of(context).pop();
}
});
}
}
},
),
);
}
}
lib/settings_page.dart
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/material.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:provider/provider.dart';
import 'l10n/app_localizations.dart';
import 'package:barcodereader/ad_manager.dart';
import 'package:barcodereader/ad_banner_widget.dart';
import 'package:barcodereader/settings_provider.dart';
// 設定画面のUI (StatefulWidget)
class SettingsPage extends StatefulWidget {
const SettingsPage({super.key});
@override
State<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
late AdManager _adManager;
// 画面内でのみ変更を保持する一時的な変数
late bool _tempContinuousScan;
late bool _tempBeepOnScan;
late bool _tempFlashOnScan;
late bool _tempLockOrientation;
late ThemeMode _tempThemeMode;
late double _tempScanInterval;
late int _tempSelectedBeepSound; // 一時変数
late Locale? _tempAppLocale;
final Map<String, String> languages = {
'en': 'English',
'bg': 'Български',
'cs': 'Čeština',
'da': 'Dansk',
'de': 'Deutsch',
'el': 'Ελληνικά',
'es': 'Español',
'et': 'Eesti',
'fi': 'Suomi',
'fr': 'Français',
'hu': 'Magyar',
'it': 'Italiano',
'ja': '日本語',
'lt': 'Lietuvių',
'lv': 'Latviešu',
'nl': 'Nederlands',
'pl': 'Polski',
'pt': 'Português',
'ro': 'Română',
'ru': 'Русский',
'sk': 'Slovenčina',
'sv': 'Svenska',
'zh': '中文',
};
@override
void initState() {
super.initState();
_adManager = AdManager();
// Providerから初期値を取得して一時変数にセット
final settings = context.read<SettingsProvider>();
_tempContinuousScan = settings.continuousScan;
_tempBeepOnScan = settings.beepOnScan;
_tempFlashOnScan = settings.flashOnScan;
_tempLockOrientation = settings.lockOrientation;
_tempThemeMode = settings.themeMode;
_tempScanInterval = settings.scanInterval;
_tempSelectedBeepSound = settings.selectedBeepSound; // 初期値
_tempAppLocale = settings.appLocale;
}
@override
void dispose() {
_adManager.dispose();
super.dispose();
}
void _applyChanges() {
// Provider経由で変更を保存
final settings = context.read<SettingsProvider>();
settings.setContinuousScan(_tempContinuousScan);
settings.setBeepOnScan(_tempBeepOnScan);
settings.setFlashOnScan(_tempFlashOnScan);
settings.setLockOrientation(_tempLockOrientation);
settings.setThemeMode(_tempThemeMode);
settings.setScanInterval(_tempScanInterval);
settings.setSelectedBeepSound(_tempSelectedBeepSound); // 保存
settings.setAppLocale(_tempAppLocale);
if (mounted) {
Navigator.of(context).pop();
}
}
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.transparent,
title: Text(l.settings),
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(context).pop(),
),
actions: [
IconButton(icon: const Icon(Icons.check), onPressed: _applyChanges),
const SizedBox(width: 24),
],
),
body: ListView(
children: [
SwitchListTile(
title: Text(l.continuousScan),
value: _tempContinuousScan,
onChanged: (value) => setState(() => _tempContinuousScan = value),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l.scanInterval(
_tempScanInterval.toStringAsFixed(1),
),
),
Slider(
value: _tempScanInterval,
min: 0.0,
max: 3.0,
divisions: 15,
label: '${_tempScanInterval.toStringAsFixed(1)}s',
onChanged: (value) {
setState(() {
_tempScanInterval = value;
});
},
),
],
),
),
const Divider(),
SwitchListTile(
title: Text(l.beepOnScan),
value: _tempBeepOnScan,
onChanged: (value) => setState(() => _tempBeepOnScan = value),
),
// --- ビープ音選択 ---
ListTile(
title: Text(l.beepSound),
trailing: DropdownButton<int>(
value: _tempSelectedBeepSound,
items: List.generate(14, (index) {
final soundNum = index + 1;
return DropdownMenuItem(
value: soundNum,
child: Text(
l.beepSoundName(
soundNum.toString(),
),
),
);
}),
onChanged: (value) {
if (value != null) {
setState(() => _tempSelectedBeepSound = value);
AudioPlayer().play(AssetSource('sound/beep$value.mp3'));
}
},
),
),
SwitchListTile(
title: Text(l.flashOnScan),
value: _tempFlashOnScan,
onChanged: (value) => setState(() => _tempFlashOnScan = value),
),
const Divider(),
SwitchListTile(
title: Text(l.lockOrientation),
value: _tempLockOrientation,
onChanged: (value) => setState(() => _tempLockOrientation = value),
),
const Divider(),
ListTile(
title: Text(l.language),
trailing: DropdownButton<String>(
value: _tempAppLocale?.languageCode,
hint: Text(l.systemDefault),
items: [
DropdownMenuItem<String>(
value: null,
child: Text(l.systemDefault),
),
...languages.entries.map((entry) {
return DropdownMenuItem<String>(
value: entry.key,
child: Text(entry.value),
);
}),
],
onChanged: (value) {
setState(() {
if (value == null) {
_tempAppLocale = null;
} else {
_tempAppLocale = Locale(value);
}
});
},
),
),
ListTile(
title: Text(l.theme),
trailing: DropdownButton<ThemeMode>(
value: _tempThemeMode,
items: [
DropdownMenuItem(
value: ThemeMode.system,
child: Text(l.systemDefault),
),
DropdownMenuItem(
value: ThemeMode.light,
child: Text(l.lightTheme),
),
DropdownMenuItem(
value: ThemeMode.dark,
child: Text(l.darkTheme),
),
],
onChanged: (value) {
if (value != null) {
setState(() => _tempThemeMode = value);
}
},
),
),
const Divider(),
],
),
bottomNavigationBar: AdBannerWidget(adManager: _adManager),
);
}
}
lib/settings_provider.dart
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
// 設定値を管理するためのProvider
class SettingsProvider extends ChangeNotifier {
bool _continuousScan = false;
bool _beepOnScan = true;
bool _flashOnScan = true;
bool _lockOrientation = false;
ThemeMode _themeMode = ThemeMode.system;
double _scanInterval = 1.0;
int _selectedBeepSound = 1; // ビープ音の選択値
Locale? _appLocale;
bool get continuousScan => _continuousScan;
bool get beepOnScan => _beepOnScan;
bool get flashOnScan => _flashOnScan;
bool get lockOrientation => _lockOrientation;
ThemeMode get themeMode => _themeMode;
double get scanInterval => _scanInterval;
int get selectedBeepSound => _selectedBeepSound; // getter
Locale? get appLocale => _appLocale;
static const String _continuousScanKey = 'continuousScan';
static const String _beepOnScanKey = 'beepOnScan';
static const String _flashOnScanKey = 'flashOnScan';
static const String _lockOrientationKey = 'lockOrientation';
static const String _themeModeKey = 'themeMode';
static const String _scanIntervalKey = 'scanInterval';
static const String _selectedBeepSoundKey = 'selectedBeepSound'; // キー
static const String _appLocaleKey = 'appLocale';
SettingsProvider() {
_loadSettings();
}
// 設定をSharedPreferencesから読み込む
Future<void> _loadSettings() async {
final prefs = await SharedPreferences.getInstance();
_continuousScan = prefs.getBool(_continuousScanKey) ?? false;
_beepOnScan = prefs.getBool(_beepOnScanKey) ?? true;
_flashOnScan = prefs.getBool(_flashOnScanKey) ?? true;
_lockOrientation = prefs.getBool(_lockOrientationKey) ?? false;
final themeIndex = prefs.getInt(_themeModeKey) ?? 0;
_themeMode = ThemeMode.values[themeIndex];
_scanInterval = prefs.getDouble(_scanIntervalKey) ?? 1.0;
_selectedBeepSound = prefs.getInt(_selectedBeepSoundKey) ?? 1; // 読み込み
final languageCode = prefs.getString(_appLocaleKey);
if (languageCode != null) {
_appLocale = Locale(languageCode);
}
notifyListeners();
}
// 各設定値を更新し、SharedPreferencesに保存
Future<void> setContinuousScan(bool value) async {
_continuousScan = value;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_continuousScanKey, value);
notifyListeners();
}
Future<void> setBeepOnScan(bool value) async {
_beepOnScan = value;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_beepOnScanKey, value);
notifyListeners();
}
Future<void> setFlashOnScan(bool value) async {
_flashOnScan = value;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_flashOnScanKey, value);
notifyListeners();
}
Future<void> setLockOrientation(bool value) async {
_lockOrientation = value;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_lockOrientationKey, value);
notifyListeners();
}
Future<void> setThemeMode(ThemeMode value) async {
_themeMode = value;
final prefs = await SharedPreferences.getInstance();
await prefs.setInt(_themeModeKey, value.index);
notifyListeners();
}
Future<void> setScanInterval(double value) async {
_scanInterval = value;
final prefs = await SharedPreferences.getInstance();
await prefs.setDouble(_scanIntervalKey, value);
notifyListeners();
}
Future<void> setSelectedBeepSound(int value) async {
// setter
_selectedBeepSound = value;
final prefs = await SharedPreferences.getInstance();
await prefs.setInt(_selectedBeepSoundKey, value);
notifyListeners();
}
Future<void> setAppLocale(Locale? locale) async {
_appLocale = locale;
final prefs = await SharedPreferences.getInstance();
if (locale == null) {
await prefs.remove(_appLocaleKey);
} else {
await prefs.setString(_appLocaleKey, locale.languageCode);
}
notifyListeners();
}
}