Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 44232e1361 | |||
| 36ef2a022b | |||
| bb7b1cf6b2 | |||
| 8f7d870985 | |||
| fd5a034ed4 | |||
| 10b1bf3f80 | |||
| 22d2e99400 | |||
| 398ac5af63 | |||
| 8c86f9b38d |
@@ -53,6 +53,10 @@ INCOMPLETE_*.md
|
||||
# DO NOT REMOVE FROM GITIGNORE
|
||||
NOTES.txt
|
||||
|
||||
# Local TODO for next sessions
|
||||
# DO NOT REMOVE FROM GITIGNORE
|
||||
TODO.md
|
||||
|
||||
# UI design references.
|
||||
# DO NOT REMOVE FROM GITIGNORE
|
||||
.uidesign/
|
||||
@@ -46,6 +46,14 @@ The app follows clean architecture principles with a modular structure ready for
|
||||
- Split viewport architecture with animated 250ms transitions and eased curves
|
||||
- Active end visual highlighting with blue glow borders and subtle background tinting
|
||||
|
||||
**localization framework**:
|
||||
- English and Finnish language support via flutter_localizations
|
||||
- ARB (Application Resource Bundle) file structure with automatic code generation
|
||||
- Comprehensive UI string localization across all components and widgets
|
||||
- Smart preservation of universal archery scoring notation (X, 10, 9, M)
|
||||
- Authentic Finnish archery terminology usage (päätös for "end", nuolet for "arrows")
|
||||
- Modern internationalization architecture ready for additional languages
|
||||
|
||||
**technical improvements**:
|
||||
- Flutter lints upgraded to 6.0.0 for modern code quality standards
|
||||
- Complete color architecture documentation in `_constants.md`
|
||||
@@ -62,7 +70,7 @@ lib/
|
||||
│ ├── constants/ # App-wide constants and color systems
|
||||
│ │ ├── scoring_colors.dart # Centralized color scheme architecture
|
||||
│ │ └── _constants.md # Color system documentation
|
||||
│ ├── localization/ # Multi-language support files
|
||||
│ ├── localization/ # Multi-language support files (ARB format)
|
||||
│ └── settings/ # User preferences and app config
|
||||
├── core/ # Business logic and models
|
||||
│ ├── calculators/ # Score calculation logic
|
||||
@@ -129,6 +137,18 @@ lib/
|
||||
|
||||
## next development priorities
|
||||
|
||||
### ✅ localization framework - completed
|
||||
|
||||
**modern internationalization system**:
|
||||
- ✅ flutter_localizations integration with automatic code generation
|
||||
- ✅ ARB (Application Resource Bundle) file structure for scalable translations
|
||||
- ✅ English (en) and Finnish (fi) language support implemented
|
||||
- ✅ comprehensive UI string localization across all scoring components
|
||||
- ✅ smart preservation of universal archery scoring notation (X, 10, 9, M)
|
||||
- ✅ authentic Finnish archery terminology with proper context usage
|
||||
- ✅ extensible architecture ready for additional languages
|
||||
- ✅ automatic AppLocalizations generation via flutter gen-l10n
|
||||
|
||||
### ✅ comprehensive end interaction system - completed
|
||||
|
||||
**sophisticated end interaction behaviors**:
|
||||
@@ -196,7 +216,7 @@ lib/
|
||||
- Improved visual polish and responsiveness
|
||||
|
||||
**phase 3 - advanced features**:
|
||||
- Multi-language support with JSON localization files
|
||||
- Additional language support (German, French, Spanish, etc.)
|
||||
- Different archery round types (FITA, NFAA, etc.)
|
||||
- Cloud synchronization with Google Drive integration
|
||||
- Social sharing capabilities (Twitter, image sharing)
|
||||
@@ -227,6 +247,7 @@ lib/
|
||||
|
||||
Current `pubspec.yaml` includes essential dependencies:
|
||||
- Flutter SDK ^3.9.0 with Material 3 theme support
|
||||
- flutter_localizations for internationalization support
|
||||
- Cupertino icons ^1.0.8 for iOS consistency
|
||||
- Flutter lints ^6.0.0 for modern code quality standards
|
||||
- Dependency overrides ensuring compatibility with latest Flutter versions
|
||||
@@ -250,8 +271,8 @@ dependencies:
|
||||
image: ^4.1.3
|
||||
pdf: ^3.10.6
|
||||
|
||||
# Internationalization
|
||||
intl: ^0.19.0
|
||||
# Additional internationalization (already included flutter_localizations)
|
||||
# intl: ^0.19.0 # Provided by flutter_localizations
|
||||
|
||||
# Development
|
||||
json_annotation: ^4.8.1
|
||||
@@ -359,6 +380,11 @@ All score calculations follow official archery rules:
|
||||
- `/lib/config/constants/scoring_colors.dart` - Centralized color scheme definitions
|
||||
- `/lib/config/constants/_constants.md` - Complete color system documentation
|
||||
|
||||
**localization system**:
|
||||
- `/lib/config/localization/app_en.arb` - English language strings (template)
|
||||
- `/lib/config/localization/app_fi.arb` - Finnish language translations
|
||||
- `/lib/l10n/` - Generated localization classes (auto-generated, excluded from git)
|
||||
|
||||
**design references**:
|
||||
- `/.uidesign/scorecard.xlsx` - Excel template with exact layout specifications
|
||||
- `/.uidesign/keypad_layout.xlsx` - Keypad button colors and arrangement
|
||||
@@ -415,6 +441,7 @@ The archery bookkeeper application has evolved into a sophisticated scoring inte
|
||||
|
||||
**current strength highlights**:
|
||||
- Comprehensive end interaction system with smart selection snapping and visual highlighting
|
||||
- Modern localization framework with English/Finnish support and extensible architecture
|
||||
- Split viewport architecture preventing UI obstruction with smooth animated transitions
|
||||
- Enhanced 4x4 keypad design with responsive flex layout and dual dismissal methods
|
||||
- Per-end descending score sorting maintaining visual stability during input
|
||||
@@ -423,6 +450,6 @@ The archery bookkeeper application has evolved into a sophisticated scoring inte
|
||||
- Robust 6-arrow layout with static 12-row grid and logical-to-visual end mapping
|
||||
- Modern development practices with flutter_lints 6.0.0 and performance-optimized constants
|
||||
|
||||
With the comprehensive end interaction system now complete, the application provides a sophisticated scoring experience that matches professional archery conventions while offering superior digital usability. The foundation is ready for the next major development phase focusing on data persistence and state management to transform the application from an advanced calculator into a complete scoring and session management solution for archery practitioners.
|
||||
With both the comprehensive end interaction system and localization framework now complete, the application provides a sophisticated, internationally-ready scoring experience that matches professional archery conventions while offering superior digital usability. The foundation is ready for the next major development phase focusing on data persistence and state management to transform the application from an advanced calculator into a complete scoring and session management solution for archery practitioners.
|
||||
|
||||
The application successfully balances authenticity to archery scoring conventions with advanced Flutter interaction patterns, creating a polished, professional foundation ready for enterprise features while maintaining exceptional code quality and visual consistency throughout.
|
||||
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2014-2018 Materialize
|
||||
Copyright (c) 2026 Mikkeli Matlock
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -6,7 +6,7 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.novoyuuparosk.archery_bookkeeper"
|
||||
namespace = "com.novoyuuparosk.archery_scorer"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
@@ -21,7 +21,7 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "com.novoyuuparosk.archery_bookkeeper"
|
||||
applicationId = "com.novoyuuparosk.archery_scorer"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application
|
||||
android:label="archery_bookkeeper"
|
||||
android:label="Archery scorer"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.novoyuuparosk.archery_bookkeeper
|
||||
package com.novoyuuparosk.archery_scorer
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Archery Bookkeeper</string>
|
||||
<string>Archery scorer</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
@@ -13,7 +13,7 @@
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>archery_bookkeeper</string>
|
||||
<string>archery_scorer</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Flutter localization configuration
|
||||
arb-dir: lib/config/localization
|
||||
template-arb-file: app_en.arb
|
||||
output-localization-file: app_localizations.dart
|
||||
output-class: AppLocalizations
|
||||
output-dir: lib/config/localization/generated
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"@@locale": "en",
|
||||
|
||||
"appTitle": "Archery Bookkeeper",
|
||||
"appVersion": "Alpha 0.1",
|
||||
|
||||
"navMainScorer": "Main (Scorer)",
|
||||
"navSettings": "Settings",
|
||||
|
||||
"arrowsPerEnd": "Arrows per end",
|
||||
"ruleset": "Ruleset",
|
||||
"rulesetIndoor": "Indoor",
|
||||
"rulesetOutdoor": "Outdoor",
|
||||
"endsCount": "{count} ends",
|
||||
"@endsCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int",
|
||||
"example": "10"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"headerEnd": "End",
|
||||
"headerArrows": "Arrows",
|
||||
"headerSumOf3": "Sum of 3",
|
||||
"headerSumOf6": "Sum of 6",
|
||||
"headerAccumulative": "Accumulative",
|
||||
"headerXCount": "X",
|
||||
"header10Count": "10",
|
||||
"header9Count": "9",
|
||||
"scoreTotal": "Total",
|
||||
|
||||
"keypadClear": "CLEAR",
|
||||
"keypadClose": "CLOSE",
|
||||
|
||||
"dialogClearScoresTitle": "Clear scores",
|
||||
"dialogClearScoresMessage": "This will clear all scores for the current session. Continue?",
|
||||
"dialogArrowChangeTitle": "Change arrow count",
|
||||
"dialogArrowChangeMessage": "Changing arrow count will clear existing scores. Continue?",
|
||||
"dialogPreserveScores": "Preserve existing scores when possible?",
|
||||
"dialogCancel": "Cancel",
|
||||
"dialogContinue": "Continue",
|
||||
"dialogYes": "Yes",
|
||||
"dialogNo": "No",
|
||||
|
||||
"settingsTitle": "Settings",
|
||||
"settingsAppearance": "Appearance",
|
||||
"settingsScoring": "Scoring",
|
||||
"settingsExport": "Export",
|
||||
"settingsAbout": "About",
|
||||
|
||||
"settingsPlaceholder": "This is a placeholder settings page.",
|
||||
"settingsFutureWillInclude": "Future settings will include:",
|
||||
"settingsColorScheme": "• Color scheme preferences",
|
||||
"settingsDefaultArrows": "• Default arrow counts",
|
||||
"settingsScoreInput": "• Score input preferences",
|
||||
"settingsExportOptions": "• Export settings",
|
||||
"settingsSessionManagement": "• Session management",
|
||||
|
||||
"dialogKeepScoresTitle": "Keep scores?",
|
||||
|
||||
"devTodoLocalization": "REPLACE ALL THESE TEXTS WITH LOCALISATION MARKERS!!"
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"@@locale": "fi",
|
||||
|
||||
"appTitle": "Jousiammunnan tuloskirja",
|
||||
"appVersion": "Alpha 0.1",
|
||||
|
||||
"navMainScorer": "Pääsivu (Tuloslaskuri)",
|
||||
"navSettings": "Asetukset",
|
||||
|
||||
"arrowsPerEnd": "Nuolia per päätös",
|
||||
"ruleset": "Sääntökokoelma",
|
||||
"rulesetIndoor": "Sisärata",
|
||||
"rulesetOutdoor": "Ulkorata",
|
||||
"endsCount": "{count} päätöstä",
|
||||
"@endsCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int",
|
||||
"example": "10"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"headerEnd": "Päätös",
|
||||
"headerArrows": "Nuolet",
|
||||
"headerSumOf3": "Summa 3",
|
||||
"headerSumOf6": "Summa 6",
|
||||
"headerAccumulative": "Yhteensä",
|
||||
"headerXCount": "X",
|
||||
"header10Count": "10",
|
||||
"header9Count": "9",
|
||||
"scoreTotal": "Yhteensä",
|
||||
|
||||
"keypadClear": "TYHJENNÄ",
|
||||
"keypadClose": "SULJE",
|
||||
|
||||
"dialogClearScoresTitle": "Tyhjennä pisteet",
|
||||
"dialogClearScoresMessage": "Tämä tyhjentää kaikki pisteet nykyiseltä kierrokselta. Jatka?",
|
||||
"dialogArrowChangeTitle": "Muuta nuolien määrää",
|
||||
"dialogArrowChangeMessage": "Nuolien määrän muuttaminen tyhjentää olemassa olevat pisteet. Jatka?",
|
||||
"dialogPreserveScores": "Säilytetäänkö olemassa olevat pisteet mahdollisuuksien mukaan?",
|
||||
"dialogCancel": "Peruuta",
|
||||
"dialogContinue": "Jatka",
|
||||
"dialogYes": "Kyllä",
|
||||
"dialogNo": "Ei",
|
||||
|
||||
"settingsTitle": "Asetukset",
|
||||
"settingsAppearance": "Ulkoasu",
|
||||
"settingsScoring": "Pisteidenlasku",
|
||||
"settingsExport": "Vienti",
|
||||
"settingsAbout": "Tietoja",
|
||||
|
||||
"settingsPlaceholder": "Tämä on väliaikainen asetukset-sivu.",
|
||||
"settingsFutureWillInclude": "Tulevat asetukset sisältävät:",
|
||||
"settingsColorScheme": "• Väriteeman asetukset",
|
||||
"settingsDefaultArrows": "• Nuolien oletusmäärät",
|
||||
"settingsScoreInput": "• Pisteidenluonnin asetukset",
|
||||
"settingsExportOptions": "• Vientiasetukset",
|
||||
"settingsSessionManagement": "• Istunnon hallinta",
|
||||
|
||||
"dialogKeepScoresTitle": "Säilytetäänkö pisteet?",
|
||||
|
||||
"devTodoLocalization": "KORVAA KAIKKI NÄMÄ TEKSTIT LOKALISOINTIMERKINNÖILLÄ!!"
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
|
||||
import 'app_localizations_en.dart';
|
||||
import 'app_localizations_fi.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// Callers can lookup localized strings with an instance of AppLocalizations
|
||||
/// returned by `AppLocalizations.of(context)`.
|
||||
///
|
||||
/// Applications need to include `AppLocalizations.delegate()` in their app's
|
||||
/// `localizationDelegates` list, and the locales they support in the app's
|
||||
/// `supportedLocales` list. For example:
|
||||
///
|
||||
/// ```dart
|
||||
/// import 'generated/app_localizations.dart';
|
||||
///
|
||||
/// return MaterialApp(
|
||||
/// localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
/// supportedLocales: AppLocalizations.supportedLocales,
|
||||
/// home: MyApplicationHome(),
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// ## Update pubspec.yaml
|
||||
///
|
||||
/// Please make sure to update your pubspec.yaml to include the following
|
||||
/// packages:
|
||||
///
|
||||
/// ```yaml
|
||||
/// dependencies:
|
||||
/// # Internationalization support.
|
||||
/// flutter_localizations:
|
||||
/// sdk: flutter
|
||||
/// intl: any # Use the pinned version from flutter_localizations
|
||||
///
|
||||
/// # Rest of dependencies
|
||||
/// ```
|
||||
///
|
||||
/// ## iOS Applications
|
||||
///
|
||||
/// iOS applications define key application metadata, including supported
|
||||
/// locales, in an Info.plist file that is built into the application bundle.
|
||||
/// To configure the locales supported by your app, you’ll need to edit this
|
||||
/// file.
|
||||
///
|
||||
/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file.
|
||||
/// Then, in the Project Navigator, open the Info.plist file under the Runner
|
||||
/// project’s Runner folder.
|
||||
///
|
||||
/// Next, select the Information Property List item, select Add Item from the
|
||||
/// Editor menu, then select Localizations from the pop-up menu.
|
||||
///
|
||||
/// Select and expand the newly-created Localizations item then, for each
|
||||
/// locale your application supports, add a new item and select the locale
|
||||
/// you wish to add from the pop-up menu in the Value field. This list should
|
||||
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
|
||||
/// property.
|
||||
abstract class AppLocalizations {
|
||||
AppLocalizations(String locale)
|
||||
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
|
||||
|
||||
final String localeName;
|
||||
|
||||
static AppLocalizations? of(BuildContext context) {
|
||||
return Localizations.of<AppLocalizations>(context, AppLocalizations);
|
||||
}
|
||||
|
||||
static const LocalizationsDelegate<AppLocalizations> delegate =
|
||||
_AppLocalizationsDelegate();
|
||||
|
||||
/// A list of this localizations delegate along with the default localizations
|
||||
/// delegates.
|
||||
///
|
||||
/// Returns a list of localizations delegates containing this delegate along with
|
||||
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
|
||||
/// and GlobalWidgetsLocalizations.delegate.
|
||||
///
|
||||
/// Additional delegates can be added by appending to this list in
|
||||
/// MaterialApp. This list does not have to be used at all if a custom list
|
||||
/// of delegates is preferred or required.
|
||||
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
|
||||
<LocalizationsDelegate<dynamic>>[
|
||||
delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
];
|
||||
|
||||
/// A list of this localizations delegate's supported locales.
|
||||
static const List<Locale> supportedLocales = <Locale>[
|
||||
Locale('en'),
|
||||
Locale('fi'),
|
||||
];
|
||||
|
||||
/// No description provided for @appTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Archery Bookkeeper'**
|
||||
String get appTitle;
|
||||
|
||||
/// No description provided for @appVersion.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Alpha 0.1'**
|
||||
String get appVersion;
|
||||
|
||||
/// No description provided for @navMainScorer.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Main (Scorer)'**
|
||||
String get navMainScorer;
|
||||
|
||||
/// No description provided for @navSettings.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Settings'**
|
||||
String get navSettings;
|
||||
|
||||
/// No description provided for @arrowsPerEnd.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Arrows per end'**
|
||||
String get arrowsPerEnd;
|
||||
|
||||
/// No description provided for @ruleset.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Ruleset'**
|
||||
String get ruleset;
|
||||
|
||||
/// No description provided for @rulesetIndoor.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Indoor'**
|
||||
String get rulesetIndoor;
|
||||
|
||||
/// No description provided for @rulesetOutdoor.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Outdoor'**
|
||||
String get rulesetOutdoor;
|
||||
|
||||
/// No description provided for @endsCount.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{count} ends'**
|
||||
String endsCount(int count);
|
||||
|
||||
/// No description provided for @headerEnd.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'End'**
|
||||
String get headerEnd;
|
||||
|
||||
/// No description provided for @headerArrows.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Arrows'**
|
||||
String get headerArrows;
|
||||
|
||||
/// No description provided for @headerSumOf3.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sum of 3'**
|
||||
String get headerSumOf3;
|
||||
|
||||
/// No description provided for @headerSumOf6.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sum of 6'**
|
||||
String get headerSumOf6;
|
||||
|
||||
/// No description provided for @headerAccumulative.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Accumulative'**
|
||||
String get headerAccumulative;
|
||||
|
||||
/// No description provided for @headerXCount.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'X'**
|
||||
String get headerXCount;
|
||||
|
||||
/// No description provided for @header10Count.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'10'**
|
||||
String get header10Count;
|
||||
|
||||
/// No description provided for @header9Count.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'9'**
|
||||
String get header9Count;
|
||||
|
||||
/// No description provided for @scoreTotal.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Total'**
|
||||
String get scoreTotal;
|
||||
|
||||
/// No description provided for @keypadClear.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'CLEAR'**
|
||||
String get keypadClear;
|
||||
|
||||
/// No description provided for @keypadClose.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'CLOSE'**
|
||||
String get keypadClose;
|
||||
|
||||
/// No description provided for @dialogClearScoresTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Clear scores'**
|
||||
String get dialogClearScoresTitle;
|
||||
|
||||
/// No description provided for @dialogClearScoresMessage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This will clear all scores for the current session. Continue?'**
|
||||
String get dialogClearScoresMessage;
|
||||
|
||||
/// No description provided for @dialogArrowChangeTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Change arrow count'**
|
||||
String get dialogArrowChangeTitle;
|
||||
|
||||
/// No description provided for @dialogArrowChangeMessage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Changing arrow count will clear existing scores. Continue?'**
|
||||
String get dialogArrowChangeMessage;
|
||||
|
||||
/// No description provided for @dialogPreserveScores.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Preserve existing scores when possible?'**
|
||||
String get dialogPreserveScores;
|
||||
|
||||
/// No description provided for @dialogCancel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Cancel'**
|
||||
String get dialogCancel;
|
||||
|
||||
/// No description provided for @dialogContinue.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Continue'**
|
||||
String get dialogContinue;
|
||||
|
||||
/// No description provided for @dialogYes.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Yes'**
|
||||
String get dialogYes;
|
||||
|
||||
/// No description provided for @dialogNo.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No'**
|
||||
String get dialogNo;
|
||||
|
||||
/// No description provided for @settingsTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Settings'**
|
||||
String get settingsTitle;
|
||||
|
||||
/// No description provided for @settingsAppearance.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Appearance'**
|
||||
String get settingsAppearance;
|
||||
|
||||
/// No description provided for @settingsScoring.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Scoring'**
|
||||
String get settingsScoring;
|
||||
|
||||
/// No description provided for @settingsExport.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Export'**
|
||||
String get settingsExport;
|
||||
|
||||
/// No description provided for @settingsAbout.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'About'**
|
||||
String get settingsAbout;
|
||||
|
||||
/// No description provided for @settingsPlaceholder.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This is a placeholder settings page.'**
|
||||
String get settingsPlaceholder;
|
||||
|
||||
/// No description provided for @settingsFutureWillInclude.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Future settings will include:'**
|
||||
String get settingsFutureWillInclude;
|
||||
|
||||
/// No description provided for @settingsColorScheme.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'• Color scheme preferences'**
|
||||
String get settingsColorScheme;
|
||||
|
||||
/// No description provided for @settingsDefaultArrows.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'• Default arrow counts'**
|
||||
String get settingsDefaultArrows;
|
||||
|
||||
/// No description provided for @settingsScoreInput.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'• Score input preferences'**
|
||||
String get settingsScoreInput;
|
||||
|
||||
/// No description provided for @settingsExportOptions.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'• Export settings'**
|
||||
String get settingsExportOptions;
|
||||
|
||||
/// No description provided for @settingsSessionManagement.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'• Session management'**
|
||||
String get settingsSessionManagement;
|
||||
|
||||
/// No description provided for @dialogKeepScoresTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Keep scores?'**
|
||||
String get dialogKeepScoresTitle;
|
||||
|
||||
/// No description provided for @devTodoLocalization.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'REPLACE ALL THESE TEXTS WITH LOCALISATION MARKERS!!'**
|
||||
String get devTodoLocalization;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
extends LocalizationsDelegate<AppLocalizations> {
|
||||
const _AppLocalizationsDelegate();
|
||||
|
||||
@override
|
||||
Future<AppLocalizations> load(Locale locale) {
|
||||
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
|
||||
}
|
||||
|
||||
@override
|
||||
bool isSupported(Locale locale) =>
|
||||
<String>['en', 'fi'].contains(locale.languageCode);
|
||||
|
||||
@override
|
||||
bool shouldReload(_AppLocalizationsDelegate old) => false;
|
||||
}
|
||||
|
||||
AppLocalizations lookupAppLocalizations(Locale locale) {
|
||||
// Lookup logic when only language code is specified.
|
||||
switch (locale.languageCode) {
|
||||
case 'en':
|
||||
return AppLocalizationsEn();
|
||||
case 'fi':
|
||||
return AppLocalizationsFi();
|
||||
}
|
||||
|
||||
throw FlutterError(
|
||||
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
|
||||
'an issue with the localizations generation tool. Please file an issue '
|
||||
'on GitHub with a reproducible sample app and the gen-l10n configuration '
|
||||
'that was used.',
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// The translations for English (`en`).
|
||||
class AppLocalizationsEn extends AppLocalizations {
|
||||
AppLocalizationsEn([String locale = 'en']) : super(locale);
|
||||
|
||||
@override
|
||||
String get appTitle => 'Archery Bookkeeper';
|
||||
|
||||
@override
|
||||
String get appVersion => 'Alpha 0.1';
|
||||
|
||||
@override
|
||||
String get navMainScorer => 'Main (Scorer)';
|
||||
|
||||
@override
|
||||
String get navSettings => 'Settings';
|
||||
|
||||
@override
|
||||
String get arrowsPerEnd => 'Arrows per end';
|
||||
|
||||
@override
|
||||
String get ruleset => 'Ruleset';
|
||||
|
||||
@override
|
||||
String get rulesetIndoor => 'Indoor';
|
||||
|
||||
@override
|
||||
String get rulesetOutdoor => 'Outdoor';
|
||||
|
||||
@override
|
||||
String endsCount(int count) {
|
||||
return '$count ends';
|
||||
}
|
||||
|
||||
@override
|
||||
String get headerEnd => 'End';
|
||||
|
||||
@override
|
||||
String get headerArrows => 'Arrows';
|
||||
|
||||
@override
|
||||
String get headerSumOf3 => 'Sum of 3';
|
||||
|
||||
@override
|
||||
String get headerSumOf6 => 'Sum of 6';
|
||||
|
||||
@override
|
||||
String get headerAccumulative => 'Accumulative';
|
||||
|
||||
@override
|
||||
String get headerXCount => 'X';
|
||||
|
||||
@override
|
||||
String get header10Count => '10';
|
||||
|
||||
@override
|
||||
String get header9Count => '9';
|
||||
|
||||
@override
|
||||
String get scoreTotal => 'Total';
|
||||
|
||||
@override
|
||||
String get keypadClear => 'CLEAR';
|
||||
|
||||
@override
|
||||
String get keypadClose => 'CLOSE';
|
||||
|
||||
@override
|
||||
String get dialogClearScoresTitle => 'Clear scores';
|
||||
|
||||
@override
|
||||
String get dialogClearScoresMessage =>
|
||||
'This will clear all scores for the current session. Continue?';
|
||||
|
||||
@override
|
||||
String get dialogArrowChangeTitle => 'Change arrow count';
|
||||
|
||||
@override
|
||||
String get dialogArrowChangeMessage =>
|
||||
'Changing arrow count will clear existing scores. Continue?';
|
||||
|
||||
@override
|
||||
String get dialogPreserveScores => 'Preserve existing scores when possible?';
|
||||
|
||||
@override
|
||||
String get dialogCancel => 'Cancel';
|
||||
|
||||
@override
|
||||
String get dialogContinue => 'Continue';
|
||||
|
||||
@override
|
||||
String get dialogYes => 'Yes';
|
||||
|
||||
@override
|
||||
String get dialogNo => 'No';
|
||||
|
||||
@override
|
||||
String get settingsTitle => 'Settings';
|
||||
|
||||
@override
|
||||
String get settingsAppearance => 'Appearance';
|
||||
|
||||
@override
|
||||
String get settingsScoring => 'Scoring';
|
||||
|
||||
@override
|
||||
String get settingsExport => 'Export';
|
||||
|
||||
@override
|
||||
String get settingsAbout => 'About';
|
||||
|
||||
@override
|
||||
String get settingsPlaceholder => 'This is a placeholder settings page.';
|
||||
|
||||
@override
|
||||
String get settingsFutureWillInclude => 'Future settings will include:';
|
||||
|
||||
@override
|
||||
String get settingsColorScheme => '• Color scheme preferences';
|
||||
|
||||
@override
|
||||
String get settingsDefaultArrows => '• Default arrow counts';
|
||||
|
||||
@override
|
||||
String get settingsScoreInput => '• Score input preferences';
|
||||
|
||||
@override
|
||||
String get settingsExportOptions => '• Export settings';
|
||||
|
||||
@override
|
||||
String get settingsSessionManagement => '• Session management';
|
||||
|
||||
@override
|
||||
String get dialogKeepScoresTitle => 'Keep scores?';
|
||||
|
||||
@override
|
||||
String get devTodoLocalization =>
|
||||
'REPLACE ALL THESE TEXTS WITH LOCALISATION MARKERS!!';
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// The translations for Finnish (`fi`).
|
||||
class AppLocalizationsFi extends AppLocalizations {
|
||||
AppLocalizationsFi([String locale = 'fi']) : super(locale);
|
||||
|
||||
@override
|
||||
String get appTitle => 'Jousiammunnan tuloskirja';
|
||||
|
||||
@override
|
||||
String get appVersion => 'Alpha 0.1';
|
||||
|
||||
@override
|
||||
String get navMainScorer => 'Pääsivu (Tuloslaskuri)';
|
||||
|
||||
@override
|
||||
String get navSettings => 'Asetukset';
|
||||
|
||||
@override
|
||||
String get arrowsPerEnd => 'Nuolia per päätös';
|
||||
|
||||
@override
|
||||
String get ruleset => 'Sääntökokoelma';
|
||||
|
||||
@override
|
||||
String get rulesetIndoor => 'Sisärata';
|
||||
|
||||
@override
|
||||
String get rulesetOutdoor => 'Ulkorata';
|
||||
|
||||
@override
|
||||
String endsCount(int count) {
|
||||
return '$count päätöstä';
|
||||
}
|
||||
|
||||
@override
|
||||
String get headerEnd => 'Päätös';
|
||||
|
||||
@override
|
||||
String get headerArrows => 'Nuolet';
|
||||
|
||||
@override
|
||||
String get headerSumOf3 => 'Summa 3';
|
||||
|
||||
@override
|
||||
String get headerSumOf6 => 'Summa 6';
|
||||
|
||||
@override
|
||||
String get headerAccumulative => 'Yhteensä';
|
||||
|
||||
@override
|
||||
String get headerXCount => 'X';
|
||||
|
||||
@override
|
||||
String get header10Count => '10';
|
||||
|
||||
@override
|
||||
String get header9Count => '9';
|
||||
|
||||
@override
|
||||
String get scoreTotal => 'Yhteensä';
|
||||
|
||||
@override
|
||||
String get keypadClear => 'TYHJENNÄ';
|
||||
|
||||
@override
|
||||
String get keypadClose => 'SULJE';
|
||||
|
||||
@override
|
||||
String get dialogClearScoresTitle => 'Tyhjennä pisteet';
|
||||
|
||||
@override
|
||||
String get dialogClearScoresMessage =>
|
||||
'Tämä tyhjentää kaikki pisteet nykyiseltä kierrokselta. Jatka?';
|
||||
|
||||
@override
|
||||
String get dialogArrowChangeTitle => 'Muuta nuolien määrää';
|
||||
|
||||
@override
|
||||
String get dialogArrowChangeMessage =>
|
||||
'Nuolien määrän muuttaminen tyhjentää olemassa olevat pisteet. Jatka?';
|
||||
|
||||
@override
|
||||
String get dialogPreserveScores =>
|
||||
'Säilytetäänkö olemassa olevat pisteet mahdollisuuksien mukaan?';
|
||||
|
||||
@override
|
||||
String get dialogCancel => 'Peruuta';
|
||||
|
||||
@override
|
||||
String get dialogContinue => 'Jatka';
|
||||
|
||||
@override
|
||||
String get dialogYes => 'Kyllä';
|
||||
|
||||
@override
|
||||
String get dialogNo => 'Ei';
|
||||
|
||||
@override
|
||||
String get settingsTitle => 'Asetukset';
|
||||
|
||||
@override
|
||||
String get settingsAppearance => 'Ulkoasu';
|
||||
|
||||
@override
|
||||
String get settingsScoring => 'Pisteidenlasku';
|
||||
|
||||
@override
|
||||
String get settingsExport => 'Vienti';
|
||||
|
||||
@override
|
||||
String get settingsAbout => 'Tietoja';
|
||||
|
||||
@override
|
||||
String get settingsPlaceholder => 'Tämä on väliaikainen asetukset-sivu.';
|
||||
|
||||
@override
|
||||
String get settingsFutureWillInclude => 'Tulevat asetukset sisältävät:';
|
||||
|
||||
@override
|
||||
String get settingsColorScheme => '• Väriteeman asetukset';
|
||||
|
||||
@override
|
||||
String get settingsDefaultArrows => '• Nuolien oletusmäärät';
|
||||
|
||||
@override
|
||||
String get settingsScoreInput => '• Pisteidenluonnin asetukset';
|
||||
|
||||
@override
|
||||
String get settingsExportOptions => '• Vientiasetukset';
|
||||
|
||||
@override
|
||||
String get settingsSessionManagement => '• Istunnon hallinta';
|
||||
|
||||
@override
|
||||
String get dialogKeepScoresTitle => 'Säilytetäänkö pisteet?';
|
||||
|
||||
@override
|
||||
String get devTodoLocalization =>
|
||||
'KORVAA KAIKKI NÄMÄ TEKSTIT LOKALISOINTIMERKINNÖILLÄ!!';
|
||||
}
|
||||
+19
-4
@@ -1,17 +1,32 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'config/localization/generated/app_localizations.dart';
|
||||
import 'presentation/pages/main_navigation_page.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const ArcheryBookkeeperApp());
|
||||
runApp(const ArcheryScorerApp());
|
||||
}
|
||||
|
||||
class ArcheryBookkeeperApp extends StatelessWidget {
|
||||
const ArcheryBookkeeperApp({super.key});
|
||||
class ArcheryScorerApp extends StatelessWidget {
|
||||
const ArcheryScorerApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Archery Bookkeeper',
|
||||
title: 'Archery scorer',
|
||||
|
||||
// Localization configuration
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: const [
|
||||
Locale('en', ''),
|
||||
Locale('fi', ''),
|
||||
],
|
||||
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: Colors.green,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../config/localization/generated/app_localizations.dart';
|
||||
import 'scoring_page.dart';
|
||||
import 'settings_page.dart';
|
||||
|
||||
@@ -12,14 +13,53 @@ class MainNavigationPage extends StatefulWidget {
|
||||
class _MainNavigationPageState extends State<MainNavigationPage> {
|
||||
int _selectedIndex = 0;
|
||||
|
||||
final List<Widget> _pages = [
|
||||
const ScoringPage(),
|
||||
// Persistent scoring state
|
||||
int _arrowsPerEnd = 3;
|
||||
int _endsPerSet = 12;
|
||||
List<List<String>> _scores = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializeScores();
|
||||
}
|
||||
|
||||
void _initializeScores() {
|
||||
_scores = List.generate(
|
||||
_endsPerSet,
|
||||
(index) => List.generate(_arrowsPerEnd, (index) => ''),
|
||||
);
|
||||
}
|
||||
|
||||
void _onArrowsPerEndChanged(int arrows) {
|
||||
setState(() {
|
||||
_arrowsPerEnd = arrows;
|
||||
_endsPerSet = arrows == 3 ? 10 : 6;
|
||||
// Note: The actual score preservation dialog logic will be handled by ScoringPage
|
||||
// This is just to keep the navigation-level state in sync
|
||||
});
|
||||
}
|
||||
|
||||
void _onScoresUpdated(List<List<String>> newScores) {
|
||||
setState(() {
|
||||
_scores = newScores;
|
||||
});
|
||||
}
|
||||
|
||||
List<Widget> get _pages => [
|
||||
ScoringPage(
|
||||
initialArrowsPerEnd: _arrowsPerEnd,
|
||||
initialEndsPerSet: _endsPerSet,
|
||||
initialScores: _scores,
|
||||
onArrowsPerEndChanged: _onArrowsPerEndChanged,
|
||||
onScoresUpdated: _onScoresUpdated,
|
||||
),
|
||||
const SettingsPage(),
|
||||
];
|
||||
|
||||
final List<String> _pageTitles = [
|
||||
'Main (Scorer)',
|
||||
'Settings',
|
||||
List<String> _getPageTitles(BuildContext context) => [
|
||||
AppLocalizations.of(context)!.navMainScorer,
|
||||
AppLocalizations.of(context)!.navSettings,
|
||||
];
|
||||
|
||||
void _onNavigationItemSelected(int index) {
|
||||
@@ -31,9 +71,12 @@ class _MainNavigationPageState extends State<MainNavigationPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final pageTitles = _getPageTitles(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(_pageTitles[_selectedIndex]),
|
||||
title: Text(pageTitles[_selectedIndex]),
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
),
|
||||
body: _pages[_selectedIndex],
|
||||
@@ -45,22 +88,22 @@ class _MainNavigationPageState extends State<MainNavigationPage> {
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.inversePrimary,
|
||||
),
|
||||
child: const Column(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'Archery Bookkeeper',
|
||||
style: TextStyle(
|
||||
l10n.appTitle,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Alpha 0.1',
|
||||
style: TextStyle(
|
||||
l10n.appVersion,
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 16,
|
||||
),
|
||||
@@ -70,22 +113,22 @@ class _MainNavigationPageState extends State<MainNavigationPage> {
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.sports),
|
||||
title: const Text('Main (Scorer)'),
|
||||
title: Text(l10n.navMainScorer),
|
||||
selected: _selectedIndex == 0,
|
||||
onTap: () => _onNavigationItemSelected(0),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.settings),
|
||||
title: const Text('Settings'),
|
||||
title: Text(l10n.navSettings),
|
||||
selected: _selectedIndex == 1,
|
||||
onTap: () => _onNavigationItemSelected(1),
|
||||
),
|
||||
const Divider(),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
'Professional archery scoring with sophisticated end interaction system.',
|
||||
style: TextStyle(
|
||||
l10n.devTodoLocalization,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey,
|
||||
),
|
||||
|
||||
@@ -1,29 +1,49 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../config/localization/generated/app_localizations.dart';
|
||||
import '../widgets/scoring_grid/scoring_grid.dart';
|
||||
import '../widgets/scoring_grid/scoring_keypad.dart';
|
||||
import '../widgets/common/toggle_switches.dart';
|
||||
|
||||
class ScoringPage extends StatefulWidget {
|
||||
const ScoringPage({super.key});
|
||||
final int initialArrowsPerEnd;
|
||||
final int initialEndsPerSet;
|
||||
final List<List<String>> initialScores;
|
||||
final Function(int) onArrowsPerEndChanged;
|
||||
final Function(List<List<String>>) onScoresUpdated;
|
||||
|
||||
const ScoringPage({
|
||||
super.key,
|
||||
required this.initialArrowsPerEnd,
|
||||
required this.initialEndsPerSet,
|
||||
required this.initialScores,
|
||||
required this.onArrowsPerEndChanged,
|
||||
required this.onScoresUpdated,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ScoringPage> createState() => _ScoringPageState();
|
||||
}
|
||||
|
||||
class _ScoringPageState extends State<ScoringPage> with SingleTickerProviderStateMixin {
|
||||
int arrowsPerEnd = 3;
|
||||
int endsPerSet = 12;
|
||||
int selectedRow = 0;
|
||||
int selectedColumn = 0;
|
||||
List<List<String>> scores = [];
|
||||
late int arrowsPerEnd;
|
||||
late int endsPerSet;
|
||||
int selectedRow = -1;
|
||||
int selectedColumn = -1;
|
||||
late List<List<String>> scores;
|
||||
bool isKeypadVisible = false;
|
||||
int? activeEndIndex;
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _keypadAnimation;
|
||||
bool _isPreservingScores = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Initialize with values from parent
|
||||
arrowsPerEnd = widget.initialArrowsPerEnd;
|
||||
endsPerSet = widget.initialEndsPerSet;
|
||||
scores = _deepCopyScores(widget.initialScores);
|
||||
|
||||
_animationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
vsync: this,
|
||||
@@ -32,7 +52,10 @@ class _ScoringPageState extends State<ScoringPage> with SingleTickerProviderStat
|
||||
parent: _animationController,
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
_initializeScores();
|
||||
}
|
||||
|
||||
List<List<String>> _deepCopyScores(List<List<String>> originalScores) {
|
||||
return originalScores.map((end) => List<String>.from(end)).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -48,13 +71,156 @@ class _ScoringPageState extends State<ScoringPage> with SingleTickerProviderStat
|
||||
);
|
||||
}
|
||||
|
||||
void _onArrowsPerEndChanged(int arrows) {
|
||||
setState(() {
|
||||
arrowsPerEnd = arrows;
|
||||
// Infer ends per set based on ruleset
|
||||
endsPerSet = arrows == 3 ? 10 : 6;
|
||||
_initializeScores();
|
||||
});
|
||||
bool _hasAnyScores() {
|
||||
for (int i = 0; i < scores.length; i++) {
|
||||
for (int j = 0; j < scores[i].length; j++) {
|
||||
if (scores[i][j].isNotEmpty) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> _onArrowsPerEndChanged(int arrows) async {
|
||||
// Check if there are existing scores and ask for confirmation
|
||||
if (_hasAnyScores()) {
|
||||
final bool? keepScores = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return AlertDialog(
|
||||
title: Text(l10n.dialogKeepScoresTitle),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: Text(l10n.dialogNo),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: Text(l10n.dialogYes),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (keepScores == null) {
|
||||
// User cancelled, don't change anything
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
final oldArrowsPerEnd = arrowsPerEnd;
|
||||
final oldScores = List<List<String>>.from(scores.map((end) => List<String>.from(end)));
|
||||
|
||||
arrowsPerEnd = arrows;
|
||||
// Infer ends per set based on ruleset
|
||||
endsPerSet = arrows == 3 ? 10 : 6;
|
||||
|
||||
if (keepScores) {
|
||||
_preserveScoresWithNewArrowCount(oldScores, oldArrowsPerEnd);
|
||||
} else {
|
||||
_initializeScores();
|
||||
}
|
||||
|
||||
// Clear active state and retract keypad
|
||||
_disarmActiveState();
|
||||
|
||||
// Notify parent of changes
|
||||
widget.onArrowsPerEndChanged(arrows);
|
||||
widget.onScoresUpdated(scores);
|
||||
});
|
||||
} else {
|
||||
// No scores exist, just switch modes normally
|
||||
setState(() {
|
||||
arrowsPerEnd = arrows;
|
||||
// Infer ends per set based on ruleset
|
||||
endsPerSet = arrows == 3 ? 10 : 6;
|
||||
_initializeScores();
|
||||
|
||||
// Clear active state and retract keypad
|
||||
_disarmActiveState();
|
||||
|
||||
// Notify parent of changes
|
||||
widget.onArrowsPerEndChanged(arrows);
|
||||
widget.onScoresUpdated(scores);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _preserveScoresWithNewArrowCount(List<List<String>> oldScores, int oldArrowsPerEnd) {
|
||||
_isPreservingScores = true; // Disable parent notifications during preservation
|
||||
|
||||
// Initialize new score structure
|
||||
scores = List.generate(
|
||||
endsPerSet,
|
||||
(index) => List.generate(arrowsPerEnd, (index) => ''),
|
||||
);
|
||||
|
||||
// Flatten old scores by visual rows, then redistribute to new structure
|
||||
List<String> allOldScores = [];
|
||||
|
||||
// Extract all scores in visual row order (12 rows total)
|
||||
for (int visualRow = 0; visualRow < 12; visualRow++) {
|
||||
if (oldArrowsPerEnd == 3) {
|
||||
// 3-arrow mode: 1 visual row = 1 logical end
|
||||
if (visualRow < oldScores.length) {
|
||||
for (int col = 0; col < 3 && col < oldScores[visualRow].length; col++) {
|
||||
allOldScores.add(oldScores[visualRow][col]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 6-arrow mode: 2 visual rows = 1 logical end
|
||||
int logicalEnd = visualRow ~/ 2;
|
||||
int rowInEnd = visualRow % 2;
|
||||
if (logicalEnd < oldScores.length) {
|
||||
for (int col = 0; col < 3; col++) {
|
||||
int arrowIndex = (rowInEnd * 3) + col;
|
||||
if (arrowIndex < oldScores[logicalEnd].length) {
|
||||
allOldScores.add(oldScores[logicalEnd][arrowIndex]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Redistribute to new structure by visual rows
|
||||
int scoreIndex = 0;
|
||||
for (int visualRow = 0; visualRow < 12; visualRow++) {
|
||||
if (arrowsPerEnd == 3) {
|
||||
// 3-arrow mode: 1 visual row = 1 logical end
|
||||
if (visualRow < scores.length) {
|
||||
for (int col = 0; col < 3; col++) {
|
||||
if (scoreIndex < allOldScores.length && allOldScores[scoreIndex].isNotEmpty) {
|
||||
scores[visualRow][col] = allOldScores[scoreIndex];
|
||||
}
|
||||
scoreIndex++;
|
||||
}
|
||||
// Sort the end after adding scores
|
||||
_sortEndScores(visualRow);
|
||||
}
|
||||
} else {
|
||||
// 6-arrow mode: 2 visual rows = 1 logical end
|
||||
int logicalEnd = visualRow ~/ 2;
|
||||
int rowInEnd = visualRow % 2;
|
||||
if (logicalEnd < scores.length) {
|
||||
for (int col = 0; col < 3; col++) {
|
||||
int arrowIndex = (rowInEnd * 3) + col;
|
||||
if (arrowIndex < arrowsPerEnd && scoreIndex < allOldScores.length && allOldScores[scoreIndex].isNotEmpty) {
|
||||
scores[logicalEnd][arrowIndex] = allOldScores[scoreIndex];
|
||||
}
|
||||
scoreIndex++;
|
||||
}
|
||||
// Sort the end after completing both rows of the logical end
|
||||
if (rowInEnd == 1) {
|
||||
_sortEndScores(logicalEnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_isPreservingScores = false; // Re-enable parent notifications
|
||||
}
|
||||
|
||||
void _sortEndScores(int endIndex) {
|
||||
@@ -81,6 +247,11 @@ class _ScoringPageState extends State<ScoringPage> with SingleTickerProviderStat
|
||||
|
||||
// Update scores array
|
||||
scores[endIndex] = sortedScores;
|
||||
|
||||
// Notify parent of score changes (unless we're preserving scores)
|
||||
if (!_isPreservingScores) {
|
||||
widget.onScoresUpdated(scores);
|
||||
}
|
||||
}
|
||||
|
||||
int _findFirstEmptyCellInEnd(int endIndex) {
|
||||
@@ -113,6 +284,16 @@ class _ScoringPageState extends State<ScoringPage> with SingleTickerProviderStat
|
||||
});
|
||||
}
|
||||
|
||||
void _disarmActiveState() {
|
||||
if (isKeypadVisible) {
|
||||
_animationController.reverse();
|
||||
}
|
||||
isKeypadVisible = false;
|
||||
activeEndIndex = null;
|
||||
selectedRow = -1;
|
||||
selectedColumn = -1;
|
||||
}
|
||||
|
||||
void _onBackgroundTapped() {
|
||||
if (isKeypadVisible) {
|
||||
_animationController.reverse().then((_) {
|
||||
@@ -140,7 +321,7 @@ class _ScoringPageState extends State<ScoringPage> with SingleTickerProviderStat
|
||||
// Clear the selected cell
|
||||
if (selectedColumn < scores[selectedRow].length && scores[selectedRow][selectedColumn].isNotEmpty) {
|
||||
scores[selectedRow][selectedColumn] = '';
|
||||
_sortEndScores(selectedRow);
|
||||
_sortEndScores(selectedRow); // This already notifies parent
|
||||
// Retract keypad and disarm after clearing
|
||||
_animationController.reverse().then((_) {
|
||||
setState(() {
|
||||
@@ -204,78 +385,78 @@ class _ScoringPageState extends State<ScoringPage> with SingleTickerProviderStat
|
||||
animation: _keypadAnimation,
|
||||
builder: (context, child) {
|
||||
// Calculate viewport heights based on animation progress
|
||||
// AppBar is in parent navigation, account for status bar and safe area
|
||||
final availableHeight = (MediaQuery.of(context).size.height -
|
||||
MediaQuery.of(context).padding.top -
|
||||
kToolbarHeight).clamp(200.0, double.infinity); // Minimum 200px
|
||||
// Since we're inside MainNavigationPage's Scaffold body, use LayoutBuilder to get actual constraints
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final availableHeight = constraints.maxHeight.clamp(200.0, double.infinity); // Minimum 200px
|
||||
|
||||
final keypadHeight = (availableHeight * 0.30 * _keypadAnimation.value).clamp(0.0, availableHeight * 0.30);
|
||||
final upperViewportHeight = (availableHeight - keypadHeight).clamp(100.0, availableHeight);
|
||||
final keypadHeight = (availableHeight * 0.30 * _keypadAnimation.value).clamp(0.0, availableHeight * 0.30);
|
||||
final upperViewportHeight = (availableHeight - keypadHeight).clamp(100.0, availableHeight);
|
||||
|
||||
return Scaffold(
|
||||
body: GestureDetector(
|
||||
onTap: _onBackgroundTapped,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Column(
|
||||
children: [
|
||||
// Upper viewport - Settings and Scoring grid (scrollable)
|
||||
SizedBox(
|
||||
height: upperViewportHeight,
|
||||
child: Column(
|
||||
children: [
|
||||
// Settings toggles
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: ToggleSwitches(
|
||||
arrowsPerEnd: arrowsPerEnd,
|
||||
onArrowsPerEndChanged: _onArrowsPerEndChanged,
|
||||
),
|
||||
),
|
||||
|
||||
// Scoring grid (scrollable within upper viewport)
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: ScoringGrid(
|
||||
scores: scores,
|
||||
return GestureDetector(
|
||||
onTap: _onBackgroundTapped,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Column(
|
||||
children: [
|
||||
// Upper viewport - Settings and Scoring grid (scrollable)
|
||||
SizedBox(
|
||||
height: upperViewportHeight,
|
||||
child: Column(
|
||||
children: [
|
||||
// Settings toggles
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: ToggleSwitches(
|
||||
arrowsPerEnd: arrowsPerEnd,
|
||||
endsPerSet: endsPerSet,
|
||||
selectedRow: selectedRow,
|
||||
selectedColumn: selectedColumn,
|
||||
activeEndIndex: activeEndIndex,
|
||||
onCellSelected: _onCellSelected,
|
||||
onArrowsPerEndChanged: _onArrowsPerEndChanged,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Lower viewport - Keypad (animated height)
|
||||
if (keypadHeight > 0)
|
||||
SizedBox(
|
||||
height: keypadHeight,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
border: Border(
|
||||
top: BorderSide(color: Colors.grey.shade300, width: 1),
|
||||
// Scoring grid (scrollable within upper viewport)
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: ScoringGrid(
|
||||
scores: scores,
|
||||
arrowsPerEnd: arrowsPerEnd,
|
||||
endsPerSet: endsPerSet,
|
||||
selectedRow: selectedRow,
|
||||
selectedColumn: selectedColumn,
|
||||
activeEndIndex: activeEndIndex,
|
||||
onCellSelected: _onCellSelected,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Opacity(
|
||||
opacity: _keypadAnimation.value,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: ScoringKeypad(
|
||||
onScoreInput: _onScoreInput,
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Lower viewport - Keypad (animated height)
|
||||
if (keypadHeight > 0)
|
||||
SizedBox(
|
||||
height: keypadHeight,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
border: Border(
|
||||
top: BorderSide(color: Colors.grey.shade300, width: 1),
|
||||
),
|
||||
),
|
||||
child: Opacity(
|
||||
opacity: _keypadAnimation.value,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: ScoringKeypad(
|
||||
onScoreInput: _onScoreInput,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,49 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../config/localization/generated/app_localizations.dart';
|
||||
|
||||
class SettingsPage extends StatelessWidget {
|
||||
const SettingsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Settings'),
|
||||
title: Text(l10n.settingsTitle),
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
),
|
||||
body: const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Settings',
|
||||
style: TextStyle(
|
||||
l10n.settingsTitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'This is a placeholder settings page.',
|
||||
style: TextStyle(
|
||||
l10n.settingsPlaceholder,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Future settings will include:',
|
||||
style: TextStyle(
|
||||
l10n.settingsFutureWillInclude,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text('• Color scheme preferences'),
|
||||
Text('• Default arrow counts'),
|
||||
Text('• Score input preferences'),
|
||||
Text('• Export settings'),
|
||||
Text('• Session management'),
|
||||
const SizedBox(height: 8),
|
||||
Text(l10n.settingsColorScheme),
|
||||
Text(l10n.settingsDefaultArrows),
|
||||
Text(l10n.settingsScoreInput),
|
||||
Text(l10n.settingsExportOptions),
|
||||
Text(l10n.settingsSessionManagement),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../config/localization/generated/app_localizations.dart';
|
||||
|
||||
class ToggleSwitches extends StatelessWidget {
|
||||
final int arrowsPerEnd;
|
||||
@@ -15,18 +16,21 @@ class ToggleSwitches extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildToggleGroup(
|
||||
'Arrows per end',
|
||||
context,
|
||||
l10n.arrowsPerEnd,
|
||||
[3, 6],
|
||||
arrowsPerEnd,
|
||||
onArrowsPerEndChanged,
|
||||
),
|
||||
_buildRulesetInfo(),
|
||||
_buildRulesetInfo(context),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -34,6 +38,7 @@ class ToggleSwitches extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildToggleGroup(
|
||||
BuildContext context,
|
||||
String label,
|
||||
List<int> options,
|
||||
int currentValue,
|
||||
@@ -66,12 +71,14 @@ class ToggleSwitches extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRulesetInfo() {
|
||||
final rulesetName = arrowsPerEnd == 3 ? 'Indoor' : 'Outdoor';
|
||||
Widget _buildRulesetInfo(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final rulesetName = arrowsPerEnd == 3 ? l10n.rulesetIndoor : l10n.rulesetOutdoor;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
'Ruleset',
|
||||
l10n.ruleset,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
@@ -95,7 +102,7 @@ class ToggleSwitches extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'$endsPerSet ends',
|
||||
l10n.endsCount(endsPerSet),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.blue.shade600,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'scoring_cell.dart';
|
||||
import '../../../config/constants/scoring_colors.dart';
|
||||
import '../../../config/localization/generated/app_localizations.dart';
|
||||
|
||||
class ScoringGrid extends StatelessWidget {
|
||||
final List<List<String>> scores;
|
||||
@@ -139,6 +140,8 @@ class ScoringGrid extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade400),
|
||||
@@ -161,7 +164,7 @@ class ScoringGrid extends StatelessWidget {
|
||||
// End column: ~13% (50/395)
|
||||
Expanded(
|
||||
flex: 13,
|
||||
child: Center(child: Text('End\\Shot', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12))),
|
||||
child: Center(child: Text(l10n.headerEnd, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12))),
|
||||
),
|
||||
// Arrow columns: ~42% (165/395)
|
||||
Expanded(
|
||||
@@ -173,7 +176,7 @@ class ScoringGrid extends StatelessWidget {
|
||||
color: Colors.blue.shade100,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Text('Arrows', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
child: Text(l10n.headerArrows, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -182,7 +185,7 @@ class ScoringGrid extends StatelessWidget {
|
||||
flex: 14,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Sum of 3',
|
||||
l10n.headerSumOf3,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
@@ -196,7 +199,7 @@ class ScoringGrid extends StatelessWidget {
|
||||
flex: 14,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Sum of 6',
|
||||
l10n.headerSumOf6,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
@@ -207,7 +210,7 @@ class ScoringGrid extends StatelessWidget {
|
||||
// Accumulative: ~17% (70/395)
|
||||
Expanded(
|
||||
flex: 17,
|
||||
child: Center(child: Text('Accumulative', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12))),
|
||||
child: Center(child: Text(l10n.headerAccumulative, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12))),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -410,14 +413,15 @@ class ScoringGrid extends StatelessWidget {
|
||||
top: BorderSide(color: Colors.grey.shade400, width: 2),
|
||||
),
|
||||
),
|
||||
child: _buildCountingSection(),
|
||||
child: _buildCountingSection(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCountingSection() {
|
||||
Widget _buildCountingSection(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final counts = _calculateXAndTenCounts();
|
||||
final finalScore = _calculateAccumulative(scores.length - 1);
|
||||
|
||||
@@ -494,7 +498,7 @@ class ScoringGrid extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('Final score', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
Text(l10n.scoreTotal, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
Text(
|
||||
finalScore.toString(),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 24),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../config/constants/scoring_colors.dart';
|
||||
import '../../../config/localization/generated/app_localizations.dart';
|
||||
|
||||
class ScoringKeypad extends StatelessWidget {
|
||||
final Function(String) onScoreInput;
|
||||
@@ -11,6 +12,8 @@ class ScoringKeypad extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
// Score buttons: 4 rows × 3 columns
|
||||
final List<List<String>> scoreLayout = [
|
||||
['X', '10', '9'],
|
||||
@@ -21,8 +24,8 @@ class ScoringKeypad extends StatelessWidget {
|
||||
|
||||
// Functional buttons: 4 rows × 1 column
|
||||
final List<String> functionalButtons = [
|
||||
'CLEAR',
|
||||
'CLOSE',
|
||||
l10n.keypadClear,
|
||||
l10n.keypadClose,
|
||||
'', // Reserved for future functions
|
||||
'', // Reserved for future functions
|
||||
];
|
||||
@@ -32,9 +35,9 @@ class ScoringKeypad extends StatelessWidget {
|
||||
// Use Flex widgets for proper percentage distribution
|
||||
return Row(
|
||||
children: [
|
||||
// Score buttons section (75% width)
|
||||
// Score buttons section (77% width)
|
||||
Expanded(
|
||||
flex: 75,
|
||||
flex: 77,
|
||||
child: Column(
|
||||
children: List.generate(4, (rowIndex) {
|
||||
return Expanded(
|
||||
@@ -59,9 +62,9 @@ class ScoringKeypad extends StatelessWidget {
|
||||
|
||||
const SizedBox(width: 2),
|
||||
|
||||
// Functional buttons section (25% width)
|
||||
// Functional buttons section (23% width)
|
||||
Expanded(
|
||||
flex: 25,
|
||||
flex: 23,
|
||||
child: Column(
|
||||
children: List.generate(4, (index) {
|
||||
final buttonText = functionalButtons[index];
|
||||
@@ -83,15 +86,25 @@ class ScoringKeypad extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildKeypadButton(BuildContext context, String score, {bool isFunctional = false}) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
// Use different styling for functional buttons
|
||||
Color buttonColor;
|
||||
Color textColor;
|
||||
|
||||
// Map display text to internal values for callback
|
||||
String internalValue = score;
|
||||
if (score == l10n.keypadClear) {
|
||||
internalValue = 'CLEAR';
|
||||
} else if (score == l10n.keypadClose) {
|
||||
internalValue = 'CLOSE';
|
||||
}
|
||||
|
||||
if (isFunctional) {
|
||||
if (score == 'CLEAR') {
|
||||
if (internalValue == 'CLEAR') {
|
||||
buttonColor = Colors.red.shade400;
|
||||
textColor = Colors.white;
|
||||
} else if (score == 'CLOSE') {
|
||||
} else if (internalValue == 'CLOSE') {
|
||||
buttonColor = Colors.blue.shade400;
|
||||
textColor = Colors.white;
|
||||
} else {
|
||||
@@ -99,12 +112,13 @@ class ScoringKeypad extends StatelessWidget {
|
||||
textColor = Colors.grey.shade600;
|
||||
}
|
||||
} else {
|
||||
// Use score directly for color mapping since scores are not localized
|
||||
buttonColor = ScoringColors.getScoreBackgroundColor(score);
|
||||
textColor = ScoringColors.getScoreTextColor(score);
|
||||
}
|
||||
|
||||
return ElevatedButton(
|
||||
onPressed: () => onScoreInput(score),
|
||||
onPressed: () => onScoreInput(internalValue),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: buttonColor,
|
||||
foregroundColor: textColor,
|
||||
|
||||
@@ -70,11 +70,24 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_localizations:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
intl:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: intl
|
||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.20.2"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
+8
-2
@@ -1,5 +1,5 @@
|
||||
name: archery_bookkeeper
|
||||
description: "Archery scoring companion."
|
||||
name: archery_scorer
|
||||
description: "Archery scorer - A Flutter application."
|
||||
# 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
|
||||
@@ -30,6 +30,9 @@ environment:
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
# flutter_localizations should automatically get the correct intl version
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
@@ -64,6 +67,9 @@ flutter:
|
||||
# the material Icons class.
|
||||
uses-material-design: true
|
||||
|
||||
# Enable code generation for localization
|
||||
generate: true
|
||||
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
|
||||
+11
-5
@@ -5,16 +5,22 @@
|
||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||
// tree, read text, and verify that the values of widget properties are correct.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:archery_bookkeeper/main.dart';
|
||||
import 'package:archery_scorer/main.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Archery Bookkeeper app loads', (WidgetTester tester) async {
|
||||
testWidgets('App loads successfully with basic structure', (WidgetTester tester) async {
|
||||
// Build our app and trigger a frame.
|
||||
await tester.pumpWidget(const ArcheryBookkeeperApp());
|
||||
await tester.pumpWidget(const ArcheryScorerApp());
|
||||
|
||||
// Verify that the app loads properly
|
||||
expect(find.text('Archery Scoring'), findsOneWidget);
|
||||
// Verify core app structure exists - completely localization-safe
|
||||
expect(find.byType(MaterialApp), findsOneWidget);
|
||||
expect(find.byType(Scaffold), findsAtLeastNWidgets(1));
|
||||
expect(find.byType(AppBar), findsAtLeastNWidgets(1));
|
||||
|
||||
// Verify drawer menu button by icon type (drawer hamburger menu)
|
||||
expect(find.byIcon(Icons.menu), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
+253
@@ -0,0 +1,253 @@
|
||||
# Flutter Rename Tool
|
||||
|
||||
A comprehensive Python script to rename Flutter applications across all platform configurations. This tool handles package names, display names, class names, and bundle identifiers consistently across Android, iOS, and Flutter files.
|
||||
|
||||
## Features
|
||||
|
||||
- **Automatic detection** of current app configuration across all platforms
|
||||
- **Default value support** - Press ENTER to keep current values unchanged
|
||||
- **Smart change detection** - Only proceed if changes are actually needed
|
||||
- **Comprehensive validation** for package names and app display names
|
||||
- **Synchronized updates** across all platform files:
|
||||
- `pubspec.yaml` - Package name and description
|
||||
- `lib/main.dart` - App title and main class name
|
||||
- `android/app/src/main/AndroidManifest.xml` - Android app label
|
||||
- `android/app/build.gradle.kts` - Android namespace and applicationId
|
||||
- `ios/Runner/Info.plist` - iOS display name and bundle name
|
||||
- **Automatic Flutter refresh** - Runs `flutter clean` and `flutter pub get` after successful rename
|
||||
- **Preview and confirmation** before making changes
|
||||
- **Safe execution** with validation and error handling
|
||||
|
||||
## Usage
|
||||
|
||||
### Interactive Mode
|
||||
|
||||
Run the script from your Flutter project root:
|
||||
|
||||
```bash
|
||||
python tools/flutter_rename.py
|
||||
```
|
||||
|
||||
The script will:
|
||||
1. Detect your current app configuration
|
||||
2. Prompt for new package name and app display name
|
||||
3. Show a preview of all changes
|
||||
4. Ask for confirmation before applying changes
|
||||
5. Update all relevant files
|
||||
|
||||
### Example Session
|
||||
|
||||
```
|
||||
Flutter App Renaming Tool
|
||||
============================================================
|
||||
|
||||
============================================================
|
||||
CURRENT APP CONFIGURATION
|
||||
============================================================
|
||||
Package Name : archery_bookkeeper
|
||||
Description : Archery scoring companion.
|
||||
App Title : Archery Bookkeeper
|
||||
Main Class : ArcheryBookkeeperApp
|
||||
Android Label : archery_bookkeeper
|
||||
Ios Display Name : Archery Bookkeeper
|
||||
Ios Bundle Name : archery_bookkeeper
|
||||
============================================================
|
||||
|
||||
============================================================
|
||||
ENTER NEW APP CONFIGURATION
|
||||
============================================================
|
||||
Press ENTER to keep current value, or type new value:
|
||||
|
||||
New package name (current: archery_scorer): archery_scorecard
|
||||
New app display name (current: Archery Scoresheet): Archery Scorecard
|
||||
|
||||
============================================================
|
||||
PREVIEW OF CHANGES
|
||||
============================================================
|
||||
Package Name : archery_bookkeeper → archery_scorecard
|
||||
App Display Name : Archery Bookkeeper → Archery Scorecard
|
||||
Main Class Name : ArcheryBookkeeperApp → ArcheryScorecardApp
|
||||
============================================================
|
||||
|
||||
Files to be updated:
|
||||
✓ pubspec.yaml
|
||||
✓ lib/main.dart
|
||||
✓ android/app/src/main/AndroidManifest.xml
|
||||
✓ android/app/build.gradle.kts
|
||||
✓ ios/Runner/Info.plist
|
||||
|
||||
This operation will modify the above files.
|
||||
Proceed with renaming? (y/N): y
|
||||
|
||||
Applying changes...
|
||||
----------------------------------------
|
||||
✓ Updated pubspec.yaml
|
||||
✓ Updated main.dart
|
||||
✓ Updated Android manifest
|
||||
✓ Updated Android build.gradle.kts
|
||||
✓ Moved and updated MainActivity.kt
|
||||
From: J:\dev\archery_bookkeeper\android\app\src\main\kotlin\com\novoyuuparosk\old_app
|
||||
To: J:\dev\archery_bookkeeper\android\app\src\main\kotlin\com\novoyuuparosk\new_app
|
||||
✓ Updated test files: widget_test.dart
|
||||
✓ Updated iOS Info.plist
|
||||
----------------------------------------
|
||||
Rename completed: 7/7 files updated successfully
|
||||
|
||||
✓ All files updated successfully!
|
||||
|
||||
Refreshing Flutter project...
|
||||
----------------------------------------
|
||||
Running 'flutter clean'...
|
||||
✓ flutter clean completed
|
||||
Running 'flutter pub get'...
|
||||
✓ flutter pub get completed
|
||||
|
||||
============================================================
|
||||
✓ Project refresh completed successfully!
|
||||
|
||||
Your Flutter app has been renamed and is ready to use.
|
||||
You can now run 'flutter run' to test the app.
|
||||
```
|
||||
|
||||
## Validation Rules
|
||||
|
||||
### Package Name
|
||||
- Must start with a lowercase letter
|
||||
- Can contain only lowercase letters, numbers, and underscores
|
||||
- Cannot start or end with underscore
|
||||
- Cannot contain consecutive underscores
|
||||
- Cannot be a Dart reserved word
|
||||
|
||||
### App Display Name
|
||||
- Must be at least 2 characters long
|
||||
- Cannot exceed 50 characters
|
||||
- Cannot be empty or contain only whitespace
|
||||
|
||||
## Files Modified
|
||||
|
||||
The script updates the following files:
|
||||
|
||||
1. **`pubspec.yaml`**
|
||||
- `name:` field (package name)
|
||||
- `description:` field (generated from app name)
|
||||
|
||||
2. **`lib/main.dart`**
|
||||
- `title:` in MaterialApp (app display name)
|
||||
- Main class name and constructor
|
||||
- `runApp()` instantiation
|
||||
|
||||
3. **`android/app/src/main/AndroidManifest.xml`**
|
||||
- `android:label` attribute in application tag
|
||||
|
||||
4. **`android/app/build.gradle.kts`**
|
||||
- `namespace` (Android namespace)
|
||||
- `applicationId` (Android application identifier)
|
||||
|
||||
5. **`android/app/src/main/kotlin/.../MainActivity.kt`**
|
||||
- Updates package declaration
|
||||
- Moves file to match new package structure
|
||||
|
||||
6. **`test/**/*.dart`**
|
||||
- Updates package imports in test files
|
||||
- Updates class name references in tests
|
||||
|
||||
7. **`ios/Runner/Info.plist`**
|
||||
- `CFBundleDisplayName` (iOS display name)
|
||||
- `CFBundleName` (iOS bundle name)
|
||||
|
||||
## Testing
|
||||
|
||||
Test the detection and validation functionality:
|
||||
|
||||
```bash
|
||||
python tools/test_detection.py
|
||||
```
|
||||
|
||||
This will show current configuration detection and test validation rules.
|
||||
|
||||
## User Experience Features
|
||||
|
||||
### Default Values
|
||||
When prompted for input, simply press ENTER to keep the current value:
|
||||
```
|
||||
New package name (current: archery_scorer):
|
||||
Using current value: archery_scorer
|
||||
```
|
||||
|
||||
### Smart Change Detection
|
||||
The script detects if no actual changes are needed:
|
||||
```
|
||||
============================================================
|
||||
No changes detected - configuration is already up to date.
|
||||
============================================================
|
||||
```
|
||||
|
||||
### Automatic Project Refresh
|
||||
After successful renaming, the script automatically runs:
|
||||
- `flutter clean` to clear build cache
|
||||
- `flutter pub get` to refresh dependencies
|
||||
|
||||
No manual steps required - your app is ready to run immediately.
|
||||
|
||||
## Post-Rename Steps
|
||||
|
||||
If automatic Flutter refresh succeeds:
|
||||
1. **Test the application**: `flutter run`
|
||||
2. **Verify on all target platforms** (Android, iOS, Web, etc.)
|
||||
|
||||
If automatic refresh fails (fallback):
|
||||
1. **Clean build cache**: `flutter clean`
|
||||
2. **Refresh dependencies**: `flutter pub get`
|
||||
3. **Test the application**: `flutter run`
|
||||
4. **Verify on all target platforms**
|
||||
|
||||
## Safety Features
|
||||
|
||||
- **Non-destructive preview** - Shows exactly what will change before applying
|
||||
- **File existence checks** - Warns about missing files
|
||||
- **Comprehensive validation** - Prevents invalid names
|
||||
- **Error handling** - Reports specific issues if updates fail
|
||||
- **Backup recommendation** - Always commit your changes to version control first
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.8 or higher
|
||||
- Must be run from Flutter project root directory
|
||||
- All target platform files should exist (created by `flutter create`)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Issue**: "File not found" warnings
|
||||
- **Solution**: Ensure you're running from the Flutter project root directory
|
||||
|
||||
**Issue**: Some files not updated
|
||||
- **Solution**: Check file permissions and ensure files are not read-only
|
||||
|
||||
**Issue**: Invalid package name error
|
||||
- **Solution**: Follow Dart package naming conventions (lowercase, underscores only)
|
||||
|
||||
**Issue**: App doesn't reflect changes after rename
|
||||
- **Solution**: Run `flutter clean` and `flutter pub get`, then rebuild
|
||||
|
||||
**Issue**: Android app crashes with ClassNotFoundException after rename
|
||||
- **Solution**: This indicates the MainActivity wasn't moved to the new package structure - the script now handles this automatically
|
||||
|
||||
**Issue**: Tests fail after renaming due to old package imports or class names
|
||||
- **Solution**: The script now automatically updates test files with new imports and class references
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Test Writing for Renamed Apps
|
||||
When writing tests, use structural widget checks instead of text-based assertions:
|
||||
|
||||
```dart
|
||||
// Good - completely localization-safe and robust
|
||||
expect(find.byType(MaterialApp), findsOneWidget);
|
||||
expect(find.byType(Scaffold), findsAtLeastNWidgets(1));
|
||||
expect(find.byIcon(Icons.menu), findsOneWidget);
|
||||
expect(find.byKey(const Key('specific-widget')), findsOneWidget);
|
||||
|
||||
// Avoid - brittle and breaks with localization
|
||||
expect(find.text('Specific Text'), findsOneWidget);
|
||||
expect(find.byTooltip('Open navigation menu'), findsOneWidget);
|
||||
```
|
||||
@@ -0,0 +1,756 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Flutter App Renaming Tool
|
||||
|
||||
A comprehensive script to rename Flutter applications across all platform configurations.
|
||||
Handles package names, display names, class names, and bundle identifiers consistently.
|
||||
|
||||
Usage: python flutter_rename.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
|
||||
class FlutterRenamer:
|
||||
def __init__(self, project_root: str = "."):
|
||||
self.project_root = Path(project_root).resolve()
|
||||
self.current_config = {}
|
||||
|
||||
def detect_current_configuration(self) -> Dict[str, str]:
|
||||
"""Detect current app configuration across all platform files."""
|
||||
config = {}
|
||||
|
||||
# Read pubspec.yaml
|
||||
pubspec_path = self.project_root / "pubspec.yaml"
|
||||
if pubspec_path.exists():
|
||||
with open(pubspec_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
if match := re.search(r'^name:\s*(.+)$', content, re.MULTILINE):
|
||||
config['package_name'] = match.group(1).strip()
|
||||
if match := re.search(r'^description:\s*["\']?([^"\']+)["\']?$', content, re.MULTILINE):
|
||||
config['description'] = match.group(1).strip()
|
||||
|
||||
# Read main.dart
|
||||
main_dart_path = self.project_root / "lib" / "main.dart"
|
||||
if main_dart_path.exists():
|
||||
with open(main_dart_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
if match := re.search(r'title:\s*["\']([^"\']+)["\']', content):
|
||||
config['app_title'] = match.group(1)
|
||||
if match := re.search(r'class\s+(\w+App)\s+extends', content):
|
||||
config['main_class'] = match.group(1)
|
||||
|
||||
# Read Android manifest
|
||||
android_manifest_path = self.project_root / "android" / "app" / "src" / "main" / "AndroidManifest.xml"
|
||||
if android_manifest_path.exists():
|
||||
try:
|
||||
with open(android_manifest_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
if match := re.search(r'android:label="([^"]*)"', content):
|
||||
config['android_label'] = match.group(1)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Read Android build.gradle.kts
|
||||
android_gradle_path = self.project_root / "android" / "app" / "build.gradle.kts"
|
||||
if android_gradle_path.exists():
|
||||
try:
|
||||
with open(android_gradle_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
if match := re.search(r'namespace\s*=\s*"([^"]*)"', content):
|
||||
config['android_namespace'] = match.group(1)
|
||||
if match := re.search(r'applicationId\s*=\s*"([^"]*)"', content):
|
||||
config['android_application_id'] = match.group(1)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Read iOS Info.plist
|
||||
ios_plist_path = self.project_root / "ios" / "Runner" / "Info.plist"
|
||||
if ios_plist_path.exists():
|
||||
try:
|
||||
with open(ios_plist_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
if match := re.search(r'<key>CFBundleDisplayName</key>\s*<string>([^<]+)</string>', content):
|
||||
config['ios_display_name'] = match.group(1)
|
||||
if match := re.search(r'<key>CFBundleName</key>\s*<string>([^<]+)</string>', content):
|
||||
config['ios_bundle_name'] = match.group(1)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.current_config = config
|
||||
return config
|
||||
|
||||
def validate_package_name(self, package_name: str) -> Tuple[bool, str]:
|
||||
"""Validate Flutter package name format."""
|
||||
if not package_name:
|
||||
return False, "Package name cannot be empty"
|
||||
|
||||
if not re.match(r'^[a-z][a-z0-9_]*$', package_name):
|
||||
return False, "Package name must start with lowercase letter and contain only lowercase letters, numbers, and underscores"
|
||||
|
||||
if package_name.startswith('_') or package_name.endswith('_'):
|
||||
return False, "Package name cannot start or end with underscore"
|
||||
|
||||
if '__' in package_name:
|
||||
return False, "Package name cannot contain consecutive underscores"
|
||||
|
||||
reserved_words = {'abstract', 'as', 'assert', 'async', 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'default', 'deferred', 'do', 'dynamic', 'else', 'enum', 'export', 'extends', 'external', 'factory', 'false', 'final', 'finally', 'for', 'function', 'get', 'hide', 'if', 'implements', 'import', 'in', 'interface', 'is', 'library', 'mixin', 'new', 'null', 'on', 'operator', 'part', 'rethrow', 'return', 'set', 'show', 'static', 'super', 'switch', 'sync', 'this', 'throw', 'true', 'try', 'typedef', 'var', 'void', 'while', 'with', 'yield'}
|
||||
if package_name in reserved_words:
|
||||
return False, f"'{package_name}' is a reserved word and cannot be used as package name"
|
||||
|
||||
return True, "Valid package name"
|
||||
|
||||
def validate_app_name(self, app_name: str) -> Tuple[bool, str]:
|
||||
"""Validate app display name."""
|
||||
if not app_name or not app_name.strip():
|
||||
return False, "App name cannot be empty"
|
||||
|
||||
if len(app_name.strip()) < 2:
|
||||
return False, "App name must be at least 2 characters long"
|
||||
|
||||
if len(app_name.strip()) > 50:
|
||||
return False, "App name should not exceed 50 characters"
|
||||
|
||||
return True, "Valid app name"
|
||||
|
||||
def generate_class_name(self, app_name: str) -> str:
|
||||
"""Generate main class name from app display name."""
|
||||
# Remove special characters and convert to PascalCase
|
||||
clean_name = re.sub(r'[^a-zA-Z0-9\s]', '', app_name)
|
||||
words = clean_name.split()
|
||||
class_name = ''.join(word.capitalize() for word in words if word)
|
||||
|
||||
if not class_name:
|
||||
class_name = "App"
|
||||
elif not class_name.endswith('App'):
|
||||
class_name += "App"
|
||||
|
||||
return class_name
|
||||
|
||||
def generate_android_package_id(self, package_name: str) -> str:
|
||||
"""Generate Android package identifier from Flutter package name."""
|
||||
# Extract the current domain from existing Android package if available
|
||||
current_android = self.current_config.get('android_namespace', '')
|
||||
if current_android:
|
||||
# Try to extract domain part (everything before the last segment)
|
||||
parts = current_android.split('.')
|
||||
if len(parts) > 1:
|
||||
domain = '.'.join(parts[:-1])
|
||||
return f"{domain}.{package_name}"
|
||||
|
||||
# Default domain if no existing Android package found
|
||||
return f"com.example.{package_name}"
|
||||
|
||||
def update_pubspec_yaml(self, package_name: str, app_name: str) -> bool:
|
||||
"""Update pubspec.yaml with new package name and description."""
|
||||
pubspec_path = self.project_root / "pubspec.yaml"
|
||||
if not pubspec_path.exists():
|
||||
print(f"Warning: {pubspec_path} not found")
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(pubspec_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Update package name
|
||||
content = re.sub(r'^name:\s*.*$', f'name: {package_name}', content, flags=re.MULTILINE)
|
||||
|
||||
# Update description
|
||||
description = f'"{app_name} - A Flutter application."'
|
||||
content = re.sub(r'^description:\s*.*$', f'description: {description}', content, flags=re.MULTILINE)
|
||||
|
||||
with open(pubspec_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
print(f"✓ Updated pubspec.yaml")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Error updating pubspec.yaml: {e}")
|
||||
return False
|
||||
|
||||
def update_main_dart(self, package_name: str, app_name: str, class_name: str) -> bool:
|
||||
"""Update main.dart with new app title and class name."""
|
||||
main_dart_path = self.project_root / "lib" / "main.dart"
|
||||
if not main_dart_path.exists():
|
||||
print(f"Warning: {main_dart_path} not found")
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(main_dart_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Update class name
|
||||
old_class_pattern = r'class\s+\w+App\s+extends'
|
||||
content = re.sub(old_class_pattern, f'class {class_name} extends', content)
|
||||
|
||||
# Update constructor
|
||||
old_constructor_pattern = r'const\s+\w+App\('
|
||||
content = re.sub(old_constructor_pattern, f'const {class_name}(', content)
|
||||
|
||||
# Update main() instantiation
|
||||
old_instantiation_pattern = r'runApp\(const\s+\w+App\(\)\)'
|
||||
content = re.sub(old_instantiation_pattern, f'runApp(const {class_name}())', content)
|
||||
|
||||
# Update title
|
||||
content = re.sub(r'title:\s*["\'][^"\']*["\']', f"title: '{app_name}'", content)
|
||||
|
||||
with open(main_dart_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
print(f"✓ Updated main.dart")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Error updating main.dart: {e}")
|
||||
return False
|
||||
|
||||
def update_android_manifest(self, app_name: str) -> bool:
|
||||
"""Update Android manifest with new app label."""
|
||||
manifest_path = self.project_root / "android" / "app" / "src" / "main" / "AndroidManifest.xml"
|
||||
if not manifest_path.exists():
|
||||
print(f"Warning: {manifest_path} not found")
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(manifest_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Update android:label
|
||||
content = re.sub(
|
||||
r'android:label="[^"]*"',
|
||||
f'android:label="{app_name}"',
|
||||
content
|
||||
)
|
||||
|
||||
with open(manifest_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
print(f"✓ Updated Android manifest")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Error updating Android manifest: {e}")
|
||||
return False
|
||||
|
||||
def update_ios_info_plist(self, package_name: str, app_name: str) -> bool:
|
||||
"""Update iOS Info.plist with new bundle and display names."""
|
||||
plist_path = self.project_root / "ios" / "Runner" / "Info.plist"
|
||||
if not plist_path.exists():
|
||||
print(f"Warning: {plist_path} not found")
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(plist_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Update CFBundleDisplayName
|
||||
content = re.sub(
|
||||
r'(<key>CFBundleDisplayName</key>\s*<string>)[^<]*(</string>)',
|
||||
f'\\g<1>{app_name}\\g<2>',
|
||||
content
|
||||
)
|
||||
|
||||
# Update CFBundleName
|
||||
content = re.sub(
|
||||
r'(<key>CFBundleName</key>\s*<string>)[^<]*(</string>)',
|
||||
f'\\g<1>{package_name}\\g<2>',
|
||||
content
|
||||
)
|
||||
|
||||
with open(plist_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
print(f"✓ Updated iOS Info.plist")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Error updating iOS Info.plist: {e}")
|
||||
return False
|
||||
|
||||
def update_android_gradle(self, android_package_id: str) -> bool:
|
||||
"""Update Android build.gradle.kts with new namespace and applicationId."""
|
||||
gradle_path = self.project_root / "android" / "app" / "build.gradle.kts"
|
||||
if not gradle_path.exists():
|
||||
print(f"Warning: {gradle_path} not found")
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(gradle_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Update namespace
|
||||
content = re.sub(
|
||||
r'namespace\s*=\s*"[^"]*"',
|
||||
f'namespace = "{android_package_id}"',
|
||||
content
|
||||
)
|
||||
|
||||
# Update applicationId
|
||||
content = re.sub(
|
||||
r'applicationId\s*=\s*"[^"]*"',
|
||||
f'applicationId = "{android_package_id}"',
|
||||
content
|
||||
)
|
||||
|
||||
with open(gradle_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
print(f"✓ Updated Android build.gradle.kts")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Error updating Android build.gradle.kts: {e}")
|
||||
return False
|
||||
|
||||
def update_android_package_structure(self, old_package_id: str, new_package_id: str) -> bool:
|
||||
"""Update Android package directory structure and MainActivity package."""
|
||||
import shutil
|
||||
|
||||
if not old_package_id or not new_package_id or old_package_id == new_package_id:
|
||||
return True # No change needed
|
||||
|
||||
# Convert package IDs to directory paths
|
||||
old_path_parts = old_package_id.split('.')
|
||||
new_path_parts = new_package_id.split('.')
|
||||
|
||||
old_package_dir = self.project_root / "android" / "app" / "src" / "main" / "kotlin" / Path(*old_path_parts)
|
||||
new_package_dir = self.project_root / "android" / "app" / "src" / "main" / "kotlin" / Path(*new_path_parts)
|
||||
|
||||
try:
|
||||
# Check if old directory exists
|
||||
if not old_package_dir.exists():
|
||||
print(f"Warning: Old Android package directory not found: {old_package_dir}")
|
||||
return True # Might be a new project or different structure
|
||||
|
||||
# Create new directory structure
|
||||
new_package_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Move MainActivity.kt and update its package declaration
|
||||
mainactivity_file = old_package_dir / "MainActivity.kt"
|
||||
if mainactivity_file.exists():
|
||||
# Read and update MainActivity.kt
|
||||
with open(mainactivity_file, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Update package declaration
|
||||
content = re.sub(
|
||||
r'^package\s+[a-z0-9_.]+',
|
||||
f'package {new_package_id}',
|
||||
content,
|
||||
flags=re.MULTILINE
|
||||
)
|
||||
|
||||
# Write to new location
|
||||
new_mainactivity_file = new_package_dir / "MainActivity.kt"
|
||||
new_package_dir.mkdir(parents=True, exist_ok=True)
|
||||
with open(new_mainactivity_file, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
# Remove old file
|
||||
mainactivity_file.unlink()
|
||||
|
||||
print(f"✓ Moved and updated MainActivity.kt")
|
||||
print(f" From: {old_package_dir}")
|
||||
print(f" To: {new_package_dir}")
|
||||
|
||||
# Clean up empty old directories
|
||||
try:
|
||||
current_dir = old_package_dir
|
||||
while current_dir != current_dir.parent and current_dir.name != "kotlin":
|
||||
if current_dir.exists() and not any(current_dir.iterdir()):
|
||||
current_dir.rmdir()
|
||||
current_dir = current_dir.parent
|
||||
else:
|
||||
break
|
||||
except OSError:
|
||||
pass # Directory not empty or other issue, that's fine
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Error updating Android package structure: {e}")
|
||||
return False
|
||||
|
||||
def update_test_files(self, old_package_name: str, new_package_name: str, old_class_name: str, new_class_name: str) -> bool:
|
||||
"""Update test files with new package imports and class names."""
|
||||
test_dir = self.project_root / "test"
|
||||
if not test_dir.exists():
|
||||
return True # No test directory, nothing to update
|
||||
|
||||
try:
|
||||
# Find all Dart files in test directory
|
||||
test_files = list(test_dir.rglob("*.dart"))
|
||||
if not test_files:
|
||||
return True # No test files found
|
||||
|
||||
updated_files = []
|
||||
for test_file in test_files:
|
||||
try:
|
||||
with open(test_file, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
original_content = content
|
||||
|
||||
# Update package imports
|
||||
if old_package_name and new_package_name:
|
||||
content = re.sub(
|
||||
rf"import\s+['\"]package:{re.escape(old_package_name)}/",
|
||||
f"import 'package:{new_package_name}/",
|
||||
content
|
||||
)
|
||||
|
||||
# Update class references
|
||||
if old_class_name and new_class_name and old_class_name != new_class_name:
|
||||
content = re.sub(
|
||||
rf"\b{re.escape(old_class_name)}\b",
|
||||
new_class_name,
|
||||
content
|
||||
)
|
||||
|
||||
# Write back if changed
|
||||
if content != original_content:
|
||||
with open(test_file, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
updated_files.append(test_file.name)
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠ Warning: Could not update test file {test_file}: {e}")
|
||||
|
||||
if updated_files:
|
||||
print(f"✓ Updated test files: {', '.join(updated_files)}")
|
||||
else:
|
||||
print("✓ No test files needed updating")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Error updating test files: {e}")
|
||||
return False
|
||||
|
||||
def find_flutter_executable(self) -> str:
|
||||
"""Find Flutter executable, handling Windows PATH issues."""
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
# First try the simple approach
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["flutter", "--version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return "flutter"
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||
pass
|
||||
|
||||
# Try finding flutter with 'where' command on Windows
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["where", "flutter"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
lines = result.stdout.strip().split('\n')
|
||||
# Prefer .bat files on Windows
|
||||
for line in lines:
|
||||
if line.endswith('.bat'):
|
||||
return line
|
||||
# Fall back to first result if no .bat found
|
||||
return lines[0]
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||
pass
|
||||
|
||||
# Common Flutter installation paths
|
||||
common_paths = [
|
||||
"J:\\dev\\flutter-sdk\\bin\\flutter.bat",
|
||||
"C:\\flutter\\bin\\flutter.bat",
|
||||
"C:\\src\\flutter\\bin\\flutter.bat",
|
||||
]
|
||||
|
||||
for path in common_paths:
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
|
||||
return None
|
||||
|
||||
def run_flutter_commands(self, auto_refresh: bool = True) -> bool:
|
||||
"""Run Flutter clean and pub get to refresh the project."""
|
||||
import subprocess
|
||||
|
||||
if not auto_refresh:
|
||||
print("Skipping automatic Flutter refresh as requested.")
|
||||
return True
|
||||
|
||||
print("\nRefreshing Flutter project...")
|
||||
print("-" * 40)
|
||||
|
||||
# Find Flutter executable
|
||||
flutter_cmd = self.find_flutter_executable()
|
||||
if not flutter_cmd:
|
||||
print("✗ Flutter executable not found.")
|
||||
print(" Please ensure Flutter is installed and accessible.")
|
||||
print(" You can manually run: flutter clean && flutter pub get")
|
||||
return False
|
||||
|
||||
print(f"Using Flutter: {flutter_cmd}")
|
||||
|
||||
try:
|
||||
# Run flutter clean
|
||||
print("Running 'flutter clean'...")
|
||||
result = subprocess.run(
|
||||
[flutter_cmd, "clean"],
|
||||
cwd=self.project_root,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
print("✓ flutter clean completed")
|
||||
else:
|
||||
print(f"⚠ flutter clean warning: {result.stderr.strip()}")
|
||||
|
||||
# Run flutter pub get
|
||||
print("Running 'flutter pub get'...")
|
||||
result = subprocess.run(
|
||||
[flutter_cmd, "pub", "get"],
|
||||
cwd=self.project_root,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
print("✓ flutter pub get completed")
|
||||
return True
|
||||
else:
|
||||
print(f"✗ flutter pub get failed: {result.stderr.strip()}")
|
||||
return False
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
print("✗ Flutter commands timed out")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"✗ Error running Flutter commands: {e}")
|
||||
return False
|
||||
|
||||
def display_current_config(self):
|
||||
"""Display current app configuration."""
|
||||
print("\n" + "="*60)
|
||||
print("CURRENT APP CONFIGURATION")
|
||||
print("="*60)
|
||||
|
||||
if not self.current_config:
|
||||
print("No configuration detected")
|
||||
return
|
||||
|
||||
for key, value in self.current_config.items():
|
||||
formatted_key = key.replace('_', ' ').title()
|
||||
print(f"{formatted_key:20}: {value}")
|
||||
|
||||
print("="*60)
|
||||
|
||||
def get_user_input(self) -> Optional[Tuple[str, str]]:
|
||||
"""Get new configuration from user input."""
|
||||
print("\n" + "="*60)
|
||||
print("ENTER NEW APP CONFIGURATION")
|
||||
print("="*60)
|
||||
print("Press ENTER to keep current value, or type new value:")
|
||||
print()
|
||||
|
||||
# Get package name
|
||||
while True:
|
||||
current_package = self.current_config.get('package_name', 'unknown')
|
||||
package_name = input(f"New package name (current: {current_package}): ").strip()
|
||||
|
||||
# Use current value if empty input
|
||||
if not package_name:
|
||||
package_name = current_package
|
||||
print(f"Using current value: {package_name}")
|
||||
|
||||
if package_name == 'unknown':
|
||||
print("Package name is required and current value is not available.")
|
||||
continue
|
||||
|
||||
valid, message = self.validate_package_name(package_name)
|
||||
if valid:
|
||||
break
|
||||
else:
|
||||
print(f"Invalid package name: {message}")
|
||||
|
||||
# Get app display name
|
||||
while True:
|
||||
current_title = self.current_config.get('app_title', 'unknown')
|
||||
app_name = input(f"New app display name (current: {current_title}): ").strip()
|
||||
|
||||
# Use current value if empty input
|
||||
if not app_name:
|
||||
app_name = current_title
|
||||
print(f"Using current value: {app_name}")
|
||||
|
||||
if app_name == 'unknown':
|
||||
print("App name is required and current value is not available.")
|
||||
continue
|
||||
|
||||
valid, message = self.validate_app_name(app_name)
|
||||
if valid:
|
||||
break
|
||||
else:
|
||||
print(f"Invalid app name: {message}")
|
||||
|
||||
return package_name, app_name
|
||||
|
||||
def confirm_changes(self, package_name: str, app_name: str, class_name: str, android_package_id: str) -> bool:
|
||||
"""Show preview and confirm changes."""
|
||||
print("\n" + "="*60)
|
||||
print("PREVIEW OF CHANGES")
|
||||
print("="*60)
|
||||
print(f"Package Name : {self.current_config.get('package_name', 'N/A')} → {package_name}")
|
||||
print(f"App Display Name : {self.current_config.get('app_title', 'N/A')} → {app_name}")
|
||||
print(f"Main Class Name : {self.current_config.get('main_class', 'N/A')} → {class_name}")
|
||||
print(f"Android Package ID : {self.current_config.get('android_namespace', 'N/A')} → {android_package_id}")
|
||||
print("="*60)
|
||||
|
||||
print("\nFiles to be updated:")
|
||||
files_to_update = [
|
||||
"pubspec.yaml",
|
||||
"lib/main.dart",
|
||||
"android/app/src/main/AndroidManifest.xml",
|
||||
"android/app/build.gradle.kts",
|
||||
"android/app/src/main/kotlin/.../MainActivity.kt",
|
||||
"ios/Runner/Info.plist",
|
||||
"test/**/*.dart (if present)"
|
||||
]
|
||||
for file_path in files_to_update:
|
||||
full_path = self.project_root / file_path
|
||||
status = "✓" if full_path.exists() else "⚠ (not found)"
|
||||
print(f" {status} {file_path}")
|
||||
|
||||
print("\nThis operation will modify the above files.")
|
||||
confirm = input("Proceed with renaming? (y/N): ").strip().lower()
|
||||
return confirm in ('y', 'yes')
|
||||
|
||||
def run_rename(self) -> bool:
|
||||
"""Execute the complete rename process."""
|
||||
print("Flutter App Renaming Tool")
|
||||
print("="*60)
|
||||
|
||||
# Detect current configuration
|
||||
self.detect_current_configuration()
|
||||
self.display_current_config()
|
||||
|
||||
# Get user input
|
||||
user_input = self.get_user_input()
|
||||
if not user_input:
|
||||
print("Rename cancelled.")
|
||||
return False
|
||||
|
||||
package_name, app_name = user_input
|
||||
class_name = self.generate_class_name(app_name)
|
||||
android_package_id = self.generate_android_package_id(package_name)
|
||||
|
||||
# Check if anything actually changed
|
||||
no_changes = (
|
||||
package_name == self.current_config.get('package_name', '') and
|
||||
app_name == self.current_config.get('app_title', '') and
|
||||
class_name == self.current_config.get('main_class', '') and
|
||||
android_package_id == self.current_config.get('android_namespace', '')
|
||||
)
|
||||
|
||||
if no_changes:
|
||||
print("\n" + "="*60)
|
||||
print("No changes detected - configuration is already up to date.")
|
||||
print("="*60)
|
||||
return True
|
||||
|
||||
# Confirm changes
|
||||
if not self.confirm_changes(package_name, app_name, class_name, android_package_id):
|
||||
print("Rename cancelled.")
|
||||
return False
|
||||
|
||||
print("\nApplying changes...")
|
||||
print("-" * 40)
|
||||
|
||||
# Apply all updates
|
||||
success_count = 0
|
||||
total_updates = 7
|
||||
|
||||
if self.update_pubspec_yaml(package_name, app_name):
|
||||
success_count += 1
|
||||
|
||||
if self.update_main_dart(package_name, app_name, class_name):
|
||||
success_count += 1
|
||||
|
||||
if self.update_android_manifest(app_name):
|
||||
success_count += 1
|
||||
|
||||
if self.update_android_gradle(android_package_id):
|
||||
success_count += 1
|
||||
|
||||
# Update Android package structure if namespace changed
|
||||
old_android_package = self.current_config.get('android_namespace', '')
|
||||
if self.update_android_package_structure(old_android_package, android_package_id):
|
||||
success_count += 1
|
||||
|
||||
if self.update_ios_info_plist(package_name, app_name):
|
||||
success_count += 1
|
||||
|
||||
# Update test files
|
||||
old_package_name = self.current_config.get('package_name', '')
|
||||
old_class_name = self.current_config.get('main_class', '')
|
||||
if self.update_test_files(old_package_name, package_name, old_class_name, class_name):
|
||||
success_count += 1
|
||||
|
||||
print("-" * 40)
|
||||
print(f"Rename completed: {success_count}/{total_updates} files updated successfully")
|
||||
|
||||
if success_count == total_updates:
|
||||
print("\n✓ All files updated successfully!")
|
||||
|
||||
# Ask about automatic Flutter refresh
|
||||
auto_refresh = input("\nRun automatic Flutter refresh (flutter clean && flutter pub get)? (Y/n): ").strip().lower()
|
||||
should_refresh = auto_refresh not in ('n', 'no')
|
||||
|
||||
# Automatically run Flutter commands to refresh the project
|
||||
flutter_success = self.run_flutter_commands(should_refresh)
|
||||
|
||||
print("\n" + "="*60)
|
||||
if flutter_success:
|
||||
print("✓ Project refresh completed successfully!")
|
||||
print("\nYour Flutter app has been renamed and is ready to use.")
|
||||
print("You can now run 'flutter run' to test the app.")
|
||||
else:
|
||||
print("⚠ Manual Flutter refresh needed:")
|
||||
print("1. Run 'flutter clean' to clear build cache")
|
||||
print("2. Run 'flutter pub get' to refresh dependencies")
|
||||
print("3. Test the app with 'flutter run'")
|
||||
|
||||
return True
|
||||
else:
|
||||
print(f"\n⚠ Some files could not be updated. Please check the errors above.")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
try:
|
||||
renamer = FlutterRenamer()
|
||||
renamer.run_rename()
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nRename cancelled by user.")
|
||||
except Exception as e:
|
||||
print(f"\nUnexpected error: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test Android package structure fix.
|
||||
"""
|
||||
|
||||
from flutter_rename import FlutterRenamer
|
||||
from pathlib import Path
|
||||
|
||||
def test_android_structure_update():
|
||||
"""Test Android package structure detection and fix logic."""
|
||||
print("Testing Android package structure update...")
|
||||
|
||||
renamer = FlutterRenamer()
|
||||
config = renamer.detect_current_configuration()
|
||||
|
||||
print(f"Current Android namespace: {config.get('android_namespace', 'N/A')}")
|
||||
|
||||
# Check current MainActivity location
|
||||
old_package = config.get('android_namespace', '')
|
||||
if old_package:
|
||||
old_parts = old_package.split('.')
|
||||
old_package_dir = renamer.project_root / "android" / "app" / "src" / "main" / "kotlin" / Path(*old_parts)
|
||||
mainactivity_file = old_package_dir / "MainActivity.kt"
|
||||
|
||||
print(f"MainActivity location: {mainactivity_file}")
|
||||
print(f"MainActivity exists: {mainactivity_file.exists()}")
|
||||
|
||||
if mainactivity_file.exists():
|
||||
with open(mainactivity_file, 'r') as f:
|
||||
content = f.read()
|
||||
print(f"Current package declaration: {content.split()[1] if 'package' in content else 'Not found'}")
|
||||
|
||||
# Test what would happen with a rename
|
||||
new_package = "com.novoyuuparosk.test_app"
|
||||
print(f"\nTesting rename to: {new_package}")
|
||||
|
||||
new_parts = new_package.split('.')
|
||||
new_package_dir = renamer.project_root / "android" / "app" / "src" / "main" / "kotlin" / Path(*new_parts)
|
||||
print(f"New MainActivity location would be: {new_package_dir / 'MainActivity.kt'}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_android_structure_update()
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify Flutter app configuration detection.
|
||||
"""
|
||||
|
||||
from flutter_rename import FlutterRenamer
|
||||
|
||||
def main():
|
||||
print("Testing Flutter app configuration detection...")
|
||||
print("=" * 60)
|
||||
|
||||
renamer = FlutterRenamer()
|
||||
config = renamer.detect_current_configuration()
|
||||
|
||||
print("Detected configuration:")
|
||||
for key, value in config.items():
|
||||
formatted_key = key.replace('_', ' ').title()
|
||||
print(f" {formatted_key:20}: {value}")
|
||||
|
||||
print("\nValidation tests:")
|
||||
|
||||
# Test package name validation
|
||||
test_packages = ['archery_scorecard', 'my-app', 'MyApp', 'archery__scorecard', '_archery', 'archery_', 'class']
|
||||
for pkg in test_packages:
|
||||
valid, msg = renamer.validate_package_name(pkg)
|
||||
status = "✓" if valid else "✗"
|
||||
print(f" {status} '{pkg}': {msg}")
|
||||
|
||||
# Test app name validation
|
||||
test_names = ['Archery Scorecard', '', 'A', 'A' * 60]
|
||||
for name in test_names:
|
||||
valid, msg = renamer.validate_app_name(name)
|
||||
status = "✓" if valid else "✗"
|
||||
print(f" {status} '{name}': {msg}")
|
||||
|
||||
# Test class name generation
|
||||
test_app_names = ['Archery Scorecard', 'My App!', 'Score-Keeper Pro', 'Simple']
|
||||
print("\nClass name generation:")
|
||||
for name in test_app_names:
|
||||
class_name = renamer.generate_class_name(name)
|
||||
print(f" '{name}' → '{class_name}'")
|
||||
|
||||
# Test Android package ID generation
|
||||
test_packages = ['archery_scorecard', 'my_app', 'simple_app']
|
||||
print("\nAndroid package ID generation:")
|
||||
for pkg in test_packages:
|
||||
android_pkg = renamer.generate_android_package_id(pkg)
|
||||
print(f" '{pkg}' → '{android_pkg}'")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test Flutter executable finding.
|
||||
"""
|
||||
|
||||
from flutter_rename import FlutterRenamer
|
||||
|
||||
def test_flutter_finder():
|
||||
"""Test Flutter executable detection."""
|
||||
renamer = FlutterRenamer()
|
||||
|
||||
print("Testing Flutter executable detection...")
|
||||
flutter_cmd = renamer.find_flutter_executable()
|
||||
|
||||
if flutter_cmd:
|
||||
print(f"✓ Found Flutter: {flutter_cmd}")
|
||||
|
||||
# Test if it actually works
|
||||
import subprocess
|
||||
try:
|
||||
result = subprocess.run([flutter_cmd, "--version"], capture_output=True, text=True, timeout=10)
|
||||
if result.returncode == 0:
|
||||
print("✓ Flutter command works!")
|
||||
print(f"Version output: {result.stdout.split()[1] if len(result.stdout.split()) > 1 else 'Unknown'}")
|
||||
else:
|
||||
print(f"✗ Flutter command failed: {result.stderr}")
|
||||
except Exception as e:
|
||||
print(f"✗ Error testing Flutter: {e}")
|
||||
else:
|
||||
print("✗ Flutter not found")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_flutter_finder()
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for improved Flutter rename script functionality.
|
||||
"""
|
||||
|
||||
from flutter_rename import FlutterRenamer
|
||||
|
||||
def test_no_changes_detection():
|
||||
"""Test that the script correctly detects when no changes are needed."""
|
||||
print("Testing no-changes detection...")
|
||||
|
||||
renamer = FlutterRenamer()
|
||||
config = renamer.detect_current_configuration()
|
||||
|
||||
# Test with current values
|
||||
if config.get('package_name') and config.get('app_title'):
|
||||
package_name = config.get('package_name')
|
||||
app_name = config.get('app_title')
|
||||
class_name = renamer.generate_class_name(app_name)
|
||||
android_package_id = renamer.generate_android_package_id(package_name)
|
||||
|
||||
# Check if no-changes detection logic works
|
||||
no_changes = (
|
||||
package_name == config.get('package_name', '') and
|
||||
app_name == config.get('app_title', '') and
|
||||
class_name == config.get('main_class', '') and
|
||||
android_package_id == config.get('android_namespace', '')
|
||||
)
|
||||
|
||||
print(f"Current package name: {config.get('package_name')}")
|
||||
print(f"Current app title: {config.get('app_title')}")
|
||||
print(f"Current class name: {config.get('main_class')}")
|
||||
print(f"Current Android namespace: {config.get('android_namespace')}")
|
||||
print()
|
||||
print(f"Generated class name: {class_name}")
|
||||
print(f"Generated Android package: {android_package_id}")
|
||||
print()
|
||||
print(f"No changes detected: {no_changes}")
|
||||
|
||||
if not no_changes:
|
||||
print("Differences found:")
|
||||
if class_name != config.get('main_class', ''):
|
||||
print(f" Class name: {config.get('main_class')} → {class_name}")
|
||||
if android_package_id != config.get('android_namespace', ''):
|
||||
print(f" Android package: {config.get('android_namespace')} → {android_package_id}")
|
||||
else:
|
||||
print("Configuration not fully detected, cannot test no-changes logic")
|
||||
|
||||
def test_current_config_display():
|
||||
"""Test current configuration detection."""
|
||||
print("\n" + "="*60)
|
||||
print("TESTING CURRENT CONFIGURATION DETECTION")
|
||||
print("="*60)
|
||||
|
||||
renamer = FlutterRenamer()
|
||||
config = renamer.detect_current_configuration()
|
||||
renamer.display_current_config()
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_current_config_display()
|
||||
print()
|
||||
test_no_changes_detection()
|
||||
Reference in New Issue
Block a user