Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions mobile-app/ios/Flutter/ephemeral/flutter_native_integration.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FLUTTER_ROOT=C:\Users\Manuel\develop\flutter
FLUTTER_APPLICATION_PATH=C:\Users\Manuel\Desktop\mobile\mobile-app
FLUTTER_FRAMEWORK_SWIFT_PACKAGE_PATH=C:\Users\Manuel\Desktop\mobile\mobile-app\ios\Flutter\ephemeral\Packages\.packages\FlutterFramework
COCOAPODS_PARALLEL_CODE_SIGN=true
FLUTTER_TARGET=lib\main.dart
FLUTTER_BUILD_DIR=build
FLUTTER_BUILD_NAME=7.5.2
FLUTTER_BUILD_NUMBER=71502
DART_OBFUSCATION=false
TRACK_WIDGET_CREATION=true
TREE_SHAKE_ICONS=false
PACKAGE_CONFIG=.dart_tool/package_config.json
2 changes: 1 addition & 1 deletion mobile-app/l10n.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
arb-dir: lib/l10n
nullable-getter: false
synthetic-package: false
untranslated-messages-file: untranslated.json
2 changes: 2 additions & 0 deletions mobile-app/lib/app/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:freecodecamp/service/firebase/remote_config_service.dart';
import 'package:freecodecamp/service/learn/learn_file_service.dart';
import 'package:freecodecamp/service/learn/learn_offline_service.dart';
import 'package:freecodecamp/service/learn/learn_service.dart';
import 'package:freecodecamp/service/symbol_bar_service.dart';
import 'package:freecodecamp/service/learn/daily_challenge_service.dart';
import 'package:freecodecamp/service/learn/daily_challenge_notification_service.dart';
import 'package:freecodecamp/service/locale_service.dart';
Expand Down Expand Up @@ -85,6 +86,7 @@ import 'package:stacked_services/stacked_services.dart';
LazySingleton(classType: LocaleService),
LazySingleton(classType: DioService),
LazySingleton(classType: NewsApiService),
LazySingleton(classType: SymbolBarService),
],
logger: StackedLogger(),
)
Expand Down
2 changes: 2 additions & 0 deletions mobile-app/lib/app/app.locator.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions mobile-app/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ import 'package:freecodecamp/service/locale_service.dart';
import 'package:freecodecamp/service/navigation/quick_actions_service.dart';
import 'package:freecodecamp/service/news/api_service.dart';
import 'package:freecodecamp/service/podcast/notification_service.dart';
import 'package:freecodecamp/service/symbol_bar_service.dart';
import 'package:freecodecamp/ui/theme/fcc_theme.dart';
import 'package:freecodecamp/utils/upgrade_controller.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:upgrader/upgrader.dart';

Expand All @@ -33,6 +35,9 @@ Future<void> main({bool testing = false}) async {
await AppAudioService().init();
await AuthenticationService().init();
await NewsApiService().init();
await locator<SymbolBarService>().init(
await SharedPreferences.getInstance(),
);
var fbApp = await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
Expand Down
257 changes: 257 additions & 0 deletions mobile-app/lib/models/symbol_set_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
/// Symbol Set Models for the customizable quick-access symbol bar
///
/// Architecture:
/// - [SymbolSetType]: Enum for predefined sets (Python, JavaScript, HTML/CSS)
/// - [SymbolSet]: Base model for any symbol set (predefined or custom)
/// - [PredefinedSymbolSet]: Sealed implementation of predefined sets
/// - [CustomSymbolSet]: User-defined symbol set
/// - [SymbolBarState]: Complete state including active set and enabled custom symbols
library;

enum SymbolSetType {
python('Python'),
javascript('JavaScript'),
htmlCss('HTML/CSS');

final String displayName;
const SymbolSetType(this.displayName);

/// Convert string to enum value (case-insensitive)
static SymbolSetType fromValue(String value) {
return SymbolSetType.values.firstWhere(
(type) => type.name.toLowerCase() == value.toLowerCase(),
orElse: () => SymbolSetType.python, // Default fallback
);
}
}

/// Abstract base class for symbol sets
/// Enables type safety and future extensibility
abstract class SymbolSet {
String get name;
List<String> get symbols;
bool get isCustom;

/// Create a copy with modified values
SymbolSet copyWith({List<String>? symbols});

/// Convert to JSON for storage
Map<String, dynamic> toJson();

/// Create from JSON
factory SymbolSet.fromJson(Map<String, dynamic> json) {
if (json['isCustom'] == true) {
return CustomSymbolSet.fromJson(json);
}
return PredefinedSymbolSet.fromJson(json);
}
}

/// Predefined symbol sets (Python, JavaScript, HTML/CSS)
/// Immutable and generated from constants
class PredefinedSymbolSet implements SymbolSet {
final SymbolSetType type;

PredefinedSymbolSet(this.type);

@override
String get name => type.displayName;

@override
List<String> get symbols {
switch (type) {
case SymbolSetType.python:
return _pythonSymbols;
case SymbolSetType.javascript:
return _javascriptSymbols;
case SymbolSetType.htmlCss:
return _htmlCssSymbols;
}
}

@override
bool get isCustom => false;

@override
SymbolSet copyWith({List<String>? symbols}) {
// Predefined sets are immutable, return self
return this;
}

@override
Map<String, dynamic> toJson() {
return {
'type': type.name,
'isCustom': false,
'symbols': symbols,
};
}

factory PredefinedSymbolSet.fromJson(Map<String, dynamic> json) {
final type = SymbolSetType.fromValue(json['type'] as String? ?? 'python');
return PredefinedSymbolSet(type);
}

// Python symbol set - ideal for Python, Ruby, and other interpreted languages
static const List<String> _pythonSymbols = [
'Tab',
'(',
')',
'[',
']',
'{',
'}',
':',
'#',
'def',
'class',
'=',
];

// JavaScript symbol set - ideal for JavaScript, TypeScript, and modern web development
static const List<String> _javascriptSymbols = [
'(',
')',
'{',
'}',
'[',
']',
';',
'=>',
'const',
'let',
'var',
'=',
];

// HTML/CSS symbol set - ideal for web markup and styling
static const List<String> _htmlCssSymbols = [
'<',
'>',
'/',
'class=',
'id=',
'div',
'p',
'span',
';',
'{',
'}',
':',
];
}

/// User-defined custom symbol set
/// Mutable and persisted to SharedPreferences
class CustomSymbolSet implements SymbolSet {
@override
final String name;

@override
final List<String> symbols;

CustomSymbolSet({
required this.name,
required this.symbols,
});

@override
bool get isCustom => true;

@override
SymbolSet copyWith({List<String>? symbols}) {
return CustomSymbolSet(
name: name,
symbols: symbols ?? this.symbols,
);
}

@override
Map<String, dynamic> toJson() {
return {
'name': name,
'symbols': symbols,
'isCustom': true,
};
}

factory CustomSymbolSet.fromJson(Map<String, dynamic> json) {
return CustomSymbolSet(
name: json['name'] as String? ?? 'Custom',
symbols: List<String>.from(json['symbols'] as List? ?? []),
);
}
}

/// Complete symbol bar state
/// Used for persistence and reactive state management
class SymbolBarState {
/// Currently active symbol set (predefined or custom)
final SymbolSet activeSet;

/// Whether custom symbols are enabled
final bool customSymbolsEnabled;

/// All user-defined custom symbol sets
final List<CustomSymbolSet> customSymbolSets;

SymbolBarState({
required this.activeSet,
this.customSymbolsEnabled = false,
this.customSymbolSets = const [],
});

/// Get the currently displayed symbols
List<String> get currentSymbols => activeSet.symbols;

/// Create a copy with modified values
SymbolBarState copyWith({
SymbolSet? activeSet,
bool? customSymbolsEnabled,
List<CustomSymbolSet>? customSymbolSets,
}) {
return SymbolBarState(
activeSet: activeSet ?? this.activeSet,
customSymbolsEnabled: customSymbolsEnabled ?? this.customSymbolsEnabled,
customSymbolSets: customSymbolSets ?? this.customSymbolSets,
);
}

/// Convert to JSON for storage
Map<String, dynamic> toJson() {
return {
'activeSet': activeSet.toJson(),
'customSymbolsEnabled': customSymbolsEnabled,
'customSymbolSets': customSymbolSets.map((s) => s.toJson()).toList(),
};
}

/// Create from JSON
factory SymbolBarState.fromJson(Map<String, dynamic> json) {
final activeSetJson = json['activeSet'] as Map<String, dynamic>?;
final activeSet = activeSetJson != null
? SymbolSet.fromJson(activeSetJson)
: PredefinedSymbolSet(SymbolSetType.python);

final customSymbolSetsJson = json['customSymbolSets'] as List?;
final customSymbolSets = (customSymbolSetsJson ?? [])
.cast<Map<String, dynamic>>()
.map((s) => CustomSymbolSet.fromJson(s))
.toList();

return SymbolBarState(
activeSet: activeSet,
customSymbolsEnabled: json['customSymbolsEnabled'] as bool? ?? false,
customSymbolSets: customSymbolSets,
);
}

/// Create default state (Python set active)
factory SymbolBarState.defaultState() {
return SymbolBarState(
activeSet: PredefinedSymbolSet(SymbolSetType.python),
customSymbolsEnabled: false,
customSymbolSets: [],
);
}
}
Loading