mirror of
https://github.com/hanskokx/arcane_framework.git
synced 2026-08-12 06:10:55 +02:00
v2.0.0: Overhaul core, services, and app integration (#6)
* [UNTESTED] Fixes notifiers and adds some additional methods. Adds tests. Changes: // ArcaneEnvironment breaking: context.read<ArcaneEnvironment>() -> ArcaneEnvironment.of(context) breaking: context.read<ArcaneEnvironment>().state -> ArcaneEnvironment.of(context).environment; // Feature flag service added: reset() // Logging service added: registerInterface() added: unregisterInterfaces() added: unregisterAllInterfaces() // ArcaneReactiveTheme fixed: currentMode, dark, light now actually emit new values when changed added: getters for lightTheme, darkTheme, and systemTheme TODO: test systemTheme Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * ArcaneReactiveTheme now optionally takes a ThemeMode parameter when calling switchTheme Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Added `of(context)` extension to `ArcaneService` * Updated changelog * Logging service: - added: reset() - added: `skipAutodetection` option for module, method, and metadata - added: unregisterInterface() - Created FileAndLineNumber mixin Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fix module/method/fileAndLineNumber calculations in logging service Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Minor fix to fileAndLineParts calculation Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Remove debug logging Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Minor change Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fixed theme - fixed: currentMode, dark, light now actually emit new values when changed - added: getters for lightTheme, darkTheme, and systemTheme - breaking: currentMode -> systemTheme - added: currentTheme - breaking: currentMode => currentTheme - change: Arcane.theme.followSystemTheme(context) required to follow system theme - change: After following the system theme, calling Arcane.theme.switchTheme() cancels following the system theme - added: ArcaneReactiveTheme is now registered in ArcaneServiceProvider when using ArcaneApp Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Removed arcane theme from service provider Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fixed late initialized value in logging service * Fixes the reactive theme service to properly follow the system brightness Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fixes tests and updates reactive theme Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fixes broken tests Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fixes bug with following/unfollowing system theme Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Re-add getters on ArcaneTheme Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Update example Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Added logStream to logger. Updated example code. Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Setting a theme style now automatically switches to that theme Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Updated theme example to set color of theme Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Added examples for feature flags Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Added environment example Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Center text Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Added environment logging Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Rename onEnvironmentChanged to switchEnvironment Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Made ArcaneServiceProvider.of(context) nullable Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Remove invalid test Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Extra log messages will not longer be added to the log stream if more than one LoggingInterface is registered Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Only send log messages to the LoggingInterfaces if the logging service has been initialized Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Check for empty interfaces before initializing them in the logger Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Moved platform brightness checking from ArcaneApp to ArcaneThemeSwitcher, where it is more appropriate Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Correctly export the theme switcher widget Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Remove unused import Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Made the switchEnvironment field in ArcaneEnvironment private (_switchEnvironment) Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Testing GH actions Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Set correct .puro.json filename in github action config Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Check different path for .puro.json Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Test different GH action workflow Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Attempt to correctly address "stable" version in .puro.json Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Another GH action attempt Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Add environment variables Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Debug Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Add puro config to git :) Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Remove debug Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * feat(framework): Overhauls framework core, services, and app integration for 2.0.0 (#5) * Made ArcaneTheme private (_ArcaneTheme) Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Updated changelog Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fix updateShouldNotify on ArcaneServiceProvider Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Update service provider Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Put service locators in an extension Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Try using a mixin instead Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Remove 'of' and 'requiredOf' locators Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Re-add service locators. They should be called as: ArcaneService.of<MyService>(context) Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Renamed getters on ArcaneService Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Renamed serviceInstances => registeredServices and added a removeService method Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Broke up arcane service into separate files Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Added requiredServiceOfType getter on ArcaneServiceProvider Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Updated services section of the readme Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Updated example to add a services example Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Removed unused variable Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Update example documentation Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Update example Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Breaking up example into smaller widgets Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Remove bloc dependency Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fixes the notifier for feature flags and updates the example Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fixed a bug in the example Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Update ServiceProvider to make service instances optional Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Remove unnecessary notifyListeners calls Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Organize theme service Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fix authentication interface and service methods for logout and login. * Fix DebugAuthInterface logout method Added environment key to ArcaneEnvironment constructor Renamed switchEnvironment parameter in ArcaneEnvironment constructor Bumped arcane_helper_utils dependency version * Enhance theme management with StreamBuilder for dynamic updates Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Add configuration files and update authentication service error handling - Introduced .vscode/settings.json and .vscode/launch.json for IDE configuration. - Updated DebugAuthInterface and ArcaneAuthenticationService to return const Result.ok() for consistency. - Added ArcaneTheme class for theme management. - Updated pubspec.yaml to change result_monad dependency version. - Modified authentication_service_test to return const Result.ok() in mock setups. Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Update dependency versions to use 'any' for arcane_helper_utils and arcane_analysis Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Remove unnecessary 'const' keyword from Result.ok calls in DebugAuthInterface methods Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Add 'const' keyword to Result.ok calls in DebugAuthInterface methods Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Enhance logging service with new lifecycle capabilities and interceptor support - Updated `LoggingInterface` to remove singleton-style initialization. - Introduced `LoggingInitializable` and `LoggingInitializationMixin` for optional lifecycle management. - Added `LogEvent` and `LogInterceptor` classes for improved logging event handling and interception. - Updated `ArcaneLogger` to support multiple logging interfaces and global interceptors. - Modified `DebugPrint` and `DebugAuthInterface` to align with new logging structure. - Enhanced tests to cover new logging features and interceptor functionality. Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Add stream-based updates for authentication, feature flags, logging, and theming - Added `statusChanges` and `signedInChanges` streams to `ArcaneAuth` for real-time authentication updates. - Introduced `enabledFeaturesChanges` stream in `ArcaneFeatureFlags` for observing feature updates. - Improved `ArcaneLogger` to maintain `logStream` usability after listener cancellation. - Updated `ArcaneTheme` to provide streams for theme mode and theme data changes. - Enhanced documentation and examples to reflect new stream APIs and usage patterns. - Added regression tests for stream listener cancellation and re-subscription across services. Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Refactor ArcaneEnvironment and AuthenticationService for improved environment management and status handling Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Add application environment management and tests for authentication status coherence Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * feat: Enhance service management with ArcaneApp provider composition This commit significantly refactors Arcane's service architecture. ArcaneApp now acts as a StatefulWidget, composing InheritedWidget- based providers for core services (features, environment, theme, auth). Service lookups (e.g., Arcane.features, context.service()) now prioritize provider-registered instances, falling back to singletons. Key changes include: - Renamed ArcaneFeatureFlags to ArcaneFeatureFlagService and ArcaneReactiveTheme to ArcaneThemeService (with typedefs). - Introduced ArcaneEnvironmentService and ArcaneEnvironmentProvider for centralized environment management. - Added BuildContext extensions (e.g., context.featureFlags) for reactive, convenient service access. - Removed .puro.json and its references, streamlining SDK setup. - Updated CHANGELOG.md to reflect a 2.0.0 release, detailing all breaking changes and new features. * chore(example): Add ignore rules for example builds * feat(logging): Introduce log interceptors and origin control Introduces the LogInterceptor class, enabling pre-processing, modification, or suppression of log events before they reach registered interfaces. The LogInterceptorContext provides contextual information such as the originating LoggingInterface. Adds a `skipAutodetection` parameter to Arcane.log, allowing control over the automatic detection of log origin (module, method, file/line). Also makes the LogEvent class extendable to support custom event types and updates existing interceptor callback signatures for consistency. * fix(theme): Update theme mode check to use currentModeOf method Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * fix(theme): Improve theme mode detection and toggling `context.isDarkMode` now reflects the app's effective theme brightness, not the raw platform brightness. This ensures `isDarkMode` accurately reports the currently rendered theme. `switchTheme()` now intelligently toggles from the *effective* theme when in `ThemeMode.system` by considering the current app brightness. `followSystemTheme()` is updated to always read the platform brightness directly, decoupling its behavior from the app's overridden theme settings. * feat(example): Improve theme and service integration demo Updates the example application to better demonstrate service and theme integration. The `FavoriteColorService` now actively sets the app's color scheme and can initialize its state from the current theme. Environment display is now reactive, and the UI responds dynamically to service registration and removal. Initial themes are now explicitly seeded. * feat(example): Add interactive persistent metadata toggle Previously, persistent metadata was added unconditionally. This change allows users to dynamically enable or disable the demo persistent metadata via a UI switch, better showcasing the API functionality. * feat(theme): Add assignment-style theme setters Introduces convenience setters for dark and light themes, allowing `Arcane.theme.dark = ...` and `Arcane.theme.light = ...` as an alternative to `setDarkTheme` and `setLightTheme`. This improves developer ergonomics and conciseness. * docs(README): Add Application Environments section to features list Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Adds `ArcaneApp.builder` to provide a provider-aware `BuildContext` for the application root. This deprecates the `ArcaneApp.child` parameter, improving developer experience for accessing Arcane's providers at build time. Updates the `ArcaneThemeSwitcher` to default to `followSystemTheme` when mounted under `ArcaneApp`, ensuring system theme behavior is enabled out of the box. Fixes `setDarkTheme` and `setLightTheme` to only update the rendered theme if their respective modes are currently active. This prevents unintended UI changes when updating the definition of an inactive theme. Updates README and examples to reflect the new `builder` API and clarified theme logic. Adds new tests for these changes. * docs(arcane_app): Clarify ArcaneApp API and provider access documentation Updates the documentation for the `ArcaneApp` constructor to explicitly detail the preferred `builder` API and the deprecated `child` parameter, including migration guidance. Also updates the list of provided services to include `ArcaneFeatureFlagsProvider`. * feat: Overhaul Arcane framework for 2.0.0 release notes Migrate Arcane to a static utility surface, removing the instantiable singleton constructor for simpler and more direct access to services. Introduce new reactive streams for environment, authentication, and theme services, enabling real-time observation of state changes. Enhance `ArcaneApp` integration with the new `builder` callback and `BuildContext` convenience accessors for services and feature flags. Update `ArcaneAuthInterface.logout` signature and revise `src` import paths, requiring consumers to update their implementations and imports. Deprecate `ArcaneApp.child` and `BuildContext.serviceOfType<T>()` in favor of new, more idiomatic APIs. Remove direct `flutter_bloc` dependency and upgrade `result_monad` to `^4.0.0`. * refactor(environment): Rename environment access getter to current This change unifies how the current application environment is accessed across Arcane's services and providers. The `state` and `environment` getters have been replaced with a consistent `current` getter, simplifying API usage and aligning with new patterns. Includes corresponding updates in example and internal code, and documents the migration in the CHANGELOG. * chore: Relax result_monad dependency constraint The `result_monad` dependency was recently upgraded to `^4.0.0`. This update relaxes the constraint to `any` to provide greater flexibility and mitigate potential dependency conflicts for consumers of Arcane, allowing them to use other compatible versions. * refactor(logging): Replace LoggingInitializationMixin with LoggingInitialization and add LoggingFeature annotation support Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * docs(changelog): Add migration steps for ArcaneThemeService and update themeMode configuration examples Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * refactor(workflow): Simplify analyze-and-test workflow by removing unused environment variables and steps Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * docs(changelog): Update Arcane.log metadata type and add migration steps for structured values Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * test: Add unit tests for authentication and feature flags functionality Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * feat(theme): track user-defined theme overrides in ArcaneThemeService Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * refactor(service): update service access methods in ArcaneServiceProvider Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * refactor(tests): format login and logout method signatures in MockAuth for improved readability Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * feat(tests): add unit tests for ArcaneThemeService functionality Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> --------- Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * fix(dependencies): update result_monad and arcane_helper_utils constraints Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * fix(version): update version from 2.0.0-dev to 2.0.0 Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * feat(auth): add callbacks for login/logout events in DebugAuthInterface fix(logging): ensure proper cancellation of log stream subscriber refactor(arcane): make built-in services list unmodifiable fix(app): update service notifier on widget updates fix(service): improve removeService method to return boolean fix(auth): update BuildContext parameter to unused in setDebug/setNormal fix(logging): correct log level reference in LogInterceptorContext documentation fix(theme): update documentation to reflect ThemeMode instead of ThemeData fix(theme): ensure unawaited cancellation of theme subscriptions chore(dependencies): update arcane_analysis version to ^1.0.4 test: make ArcaneAuthenticationService reset asynchronous in tests Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * feat(theme): enhance theme synchronization and documentation for ArcaneApp Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * fix(gitignore): add example/.metadata to .gitignore Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> --------- Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> Co-authored-by: Hans Kokx <hans.kokx@airahome.com>
This commit is contained in:
@@ -1,7 +1,21 @@
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
enum Feature {
|
||||
logging(true),
|
||||
authentication(true),
|
||||
;
|
||||
|
||||
final bool enabledAtStartup;
|
||||
const Feature(this.enabledAtStartup);
|
||||
}
|
||||
|
||||
// Some colors we'll use for our example
|
||||
const List<MaterialColor> colors = [
|
||||
Colors.red,
|
||||
Colors.orange,
|
||||
Colors.yellow,
|
||||
Colors.green,
|
||||
Colors.blue,
|
||||
Colors.purple,
|
||||
Colors.deepPurple,
|
||||
];
|
||||
|
||||
@@ -5,10 +5,7 @@ typedef Credentials = ({String email, String password});
|
||||
class DebugAuthInterface
|
||||
with ArcaneAuthAccountRegistration, ArcaneAuthPasswordManagement
|
||||
implements ArcaneAuthInterface {
|
||||
DebugAuthInterface._internal();
|
||||
|
||||
static final ArcaneAuthInterface _instance = DebugAuthInterface._internal();
|
||||
static ArcaneAuthInterface get I => _instance;
|
||||
DebugAuthInterface();
|
||||
|
||||
@override
|
||||
Future<bool> get isSignedIn => Future.value(_isSignedIn);
|
||||
@@ -25,24 +22,28 @@ class DebugAuthInterface
|
||||
);
|
||||
|
||||
@override
|
||||
Future<Result<void, String>> logout() async {
|
||||
Future<Result<void, String>> logout({
|
||||
Future<void> Function()? onLoggedOut,
|
||||
}) async {
|
||||
Arcane.log("Logging out");
|
||||
|
||||
_isSignedIn = false;
|
||||
|
||||
return Result.ok(null);
|
||||
await onLoggedOut?.call();
|
||||
|
||||
return const Result.ok(null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<void, String>> login<Credentials>({
|
||||
Credentials? input,
|
||||
Future<Result<void, String>> login<T>({
|
||||
T? input,
|
||||
Future<void> Function()? onLoggedIn,
|
||||
}) async {
|
||||
final bool alreadyLoggedIn = await isSignedIn;
|
||||
|
||||
if (alreadyLoggedIn) return Result.ok(null);
|
||||
if (alreadyLoggedIn) return const Result.ok(null);
|
||||
|
||||
final credentials = input as ({String email, String password});
|
||||
final credentials = input as Credentials;
|
||||
|
||||
final String email = credentials.email;
|
||||
final String password = credentials.password;
|
||||
@@ -51,7 +52,9 @@ class DebugAuthInterface
|
||||
|
||||
_isSignedIn = true;
|
||||
|
||||
return Result.ok(null);
|
||||
await onLoggedIn?.call();
|
||||
|
||||
return const Result.ok(null);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -59,15 +62,15 @@ class DebugAuthInterface
|
||||
T? input,
|
||||
}) async {
|
||||
Arcane.log("Re-sending verification code to $input");
|
||||
return Result.ok("Code sent");
|
||||
return const Result.ok("Code sent");
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<SignUpStep, String>> register<Credentials>({
|
||||
Credentials? input,
|
||||
Future<Result<SignUpStep, String>> register<T>({
|
||||
T? input,
|
||||
}) async {
|
||||
if (input != null) {
|
||||
final credentials = input as ({String email, String password});
|
||||
final credentials = input as Credentials;
|
||||
|
||||
final String email = credentials.email;
|
||||
final String password = credentials.password;
|
||||
@@ -75,7 +78,7 @@ class DebugAuthInterface
|
||||
Arcane.log("Creating account for $email with password $password");
|
||||
}
|
||||
|
||||
return Result.ok(SignUpStep.confirmSignUp);
|
||||
return const Result.ok(SignUpStep.confirmSignUp);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -86,7 +89,7 @@ class DebugAuthInterface
|
||||
Arcane.log(
|
||||
"Confirming registration for $username with code $confirmationCode",
|
||||
);
|
||||
return Result.ok(true);
|
||||
return const Result.ok(true);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -96,7 +99,7 @@ class DebugAuthInterface
|
||||
String? code,
|
||||
}) async {
|
||||
Arcane.log("Resetting password for $email");
|
||||
return Result.ok(true);
|
||||
return const Result.ok(true);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,28 +1,15 @@
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
import "package:example/config.dart";
|
||||
import "package:flutter/foundation.dart";
|
||||
|
||||
class DebugPrint implements LoggingInterface {
|
||||
DebugPrint._internal();
|
||||
static final DebugPrint _instance = DebugPrint._internal();
|
||||
static DebugPrint get I => _instance;
|
||||
|
||||
@override
|
||||
bool get initialized => true;
|
||||
|
||||
class DebugPrint extends LoggingInterface {
|
||||
@override
|
||||
void log(
|
||||
String message, {
|
||||
Map<String, dynamic>? metadata,
|
||||
Map<String, Object?>? metadata,
|
||||
Level? level = Level.debug,
|
||||
StackTrace? stackTrace,
|
||||
Object? extra,
|
||||
}) {
|
||||
if (Feature.logging.disabled) return;
|
||||
|
||||
debugPrint("[${level!.name}] $message ($metadata)");
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LoggingInterface?> init() async => I;
|
||||
}
|
||||
|
||||
+616
-91
@@ -1,135 +1,660 @@
|
||||
import "dart:async";
|
||||
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
import "package:example/config.dart";
|
||||
import "package:example/interfaces/debug_auth_interface.dart";
|
||||
import "package:example/interfaces/debug_print_interface.dart";
|
||||
import "package:example/services/demo_service.dart";
|
||||
import "package:example/services/favorite_color_service.dart";
|
||||
import "package:example/theme/theme.dart";
|
||||
import "package:flutter/foundation.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.dart";
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
if (kIsWeb) {
|
||||
// Work around a Flutter web debug assertion where legacy raw key messages
|
||||
// can arrive before key data and force an incompatible transit mode.
|
||||
SystemChannels.keyEvent.setMessageHandler(
|
||||
(_) async => <String, dynamic>{"handled": false},
|
||||
);
|
||||
}
|
||||
|
||||
final DebugPrint debugPrintInterface = DebugPrint();
|
||||
final DebugAuthInterface debugAuthInterface = DebugAuthInterface();
|
||||
|
||||
// If any Feature enum items are `enabledAtStartup`, enable them within Arcane.
|
||||
for (final Feature feature in Feature.values) {
|
||||
if (feature.enabledAtStartup) Arcane.features.enableFeature(feature);
|
||||
}
|
||||
|
||||
await Future.wait([
|
||||
Arcane.logger.registerInterfaces([
|
||||
DebugPrint.I,
|
||||
]),
|
||||
IdService.I.init(),
|
||||
]);
|
||||
// Register the logging interface
|
||||
await Arcane.logger.registerInterface(
|
||||
debugPrintInterface,
|
||||
interceptors: [
|
||||
LogInterceptor((event, context) {
|
||||
if (context.interface is DebugPrint && Feature.logging.disabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Arcane.logger.addPersistentMetadata({
|
||||
"session_id": IdService.I.sessionId.value,
|
||||
});
|
||||
return event;
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
await Arcane.auth.registerInterface(DebugAuthInterface.I);
|
||||
// Register the authentication interface
|
||||
await Arcane.auth.registerInterface(debugAuthInterface);
|
||||
|
||||
// Set the light and dark mode themes using our pre-defined ThemeData classes
|
||||
Arcane.theme
|
||||
..setDarkTheme(darkTheme)
|
||||
..setLightTheme(lightTheme);
|
||||
..setLightTheme(lightTheme)
|
||||
..setDarkTheme(darkTheme);
|
||||
|
||||
// Log a message that the app has been initialized
|
||||
Arcane.log(
|
||||
"Initialization complete.",
|
||||
// Set an appropriate log level
|
||||
level: Level.info,
|
||||
// The `module` and `method` are _often_ automatically determined, but they can be overridden.
|
||||
module: "main",
|
||||
method: "main",
|
||||
// Skip autodetction of the `module`, `method`, and file/line number where logs originated from.
|
||||
skipAutodetection: true,
|
||||
// Add some optional metadata to be included in this single log message. This is added to the
|
||||
// persistent metadata, if any has been set.
|
||||
metadata: {
|
||||
"ready": "true",
|
||||
},
|
||||
);
|
||||
|
||||
runApp(const MainApp());
|
||||
runApp(
|
||||
// The `ArcaneApp` widget is optional but provides Arcane's built-in
|
||||
// service, feature flag, environment, and theme integration widgets.
|
||||
// It also performs initial platform theme synchronization automatically
|
||||
// via ArcaneThemeSwitcher.
|
||||
ArcaneApp(
|
||||
builder: (context, _) => const MainApp(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class MainApp extends StatefulWidget {
|
||||
class MainApp extends StatelessWidget {
|
||||
const MainApp({super.key});
|
||||
|
||||
@override
|
||||
State<MainApp> createState() => _MainAppState();
|
||||
}
|
||||
|
||||
class _MainAppState extends State<MainApp> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ArcaneApp(
|
||||
services: [
|
||||
IdService.I,
|
||||
],
|
||||
child: MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: Arcane.theme.light,
|
||||
darkTheme: Arcane.theme.dark,
|
||||
themeMode: Arcane.theme.currentMode,
|
||||
home: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("Arcane Framework Example"),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.contrast),
|
||||
onPressed: () {
|
||||
Arcane.theme.switchTheme();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: const HomeScreen(),
|
||||
return MaterialApp(
|
||||
theme: Arcane.theme.light,
|
||||
darkTheme: Arcane.theme.dark,
|
||||
themeMode: Arcane.theme.currentModeOf(context),
|
||||
home: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("Arcane Framework Example"),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@override
|
||||
State<HomeScreen> createState() => _HomeScreenState();
|
||||
}
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool isSignedIn = Arcane.auth.isSignedIn.value;
|
||||
return Center(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
body: Column(
|
||||
children: [
|
||||
Text(
|
||||
"Authentication status: ${Arcane.auth.status.name}",
|
||||
Expanded(
|
||||
child: GridView.extent(
|
||||
maxCrossAxisExtent: 300,
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: const [
|
||||
ArcaneThemeExample(),
|
||||
ArcaneAuthExample(),
|
||||
ArcaneFeatureFlagsExample(),
|
||||
ArcaneEnvironmentExample(),
|
||||
ArcaneServicesExample(),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isSignedIn)
|
||||
ElevatedButton(
|
||||
child: const Text("Sign out"),
|
||||
onPressed: () async {
|
||||
await Arcane.auth.logOut(
|
||||
onLoggedOut: () async {
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
if (!isSignedIn)
|
||||
ElevatedButton(
|
||||
child: const Text("Sign in"),
|
||||
onPressed: () async {
|
||||
await Arcane.auth.login<Map<String, String>>(
|
||||
input: {
|
||||
"email": "email",
|
||||
"password": "password",
|
||||
},
|
||||
onLoggedIn: () async {
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
const ArcaneLoggingExample(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// * Logging
|
||||
// Arcane's logging system gives developers the power to dynamically add and
|
||||
// remove logging interfaces on-the-fly: try enabling a debug logging interface
|
||||
// when the app is running in debug mode, adding a third-party logging interface
|
||||
// when in production, and waiting until after the user has gone through the
|
||||
// login process to ask them for permission to track. Include useful metadata,
|
||||
// including persistent metadata, in your log messages. All of these things, and
|
||||
// more, are possible when using Arcane's logging system.
|
||||
class ArcaneLoggingExample extends StatefulWidget {
|
||||
const ArcaneLoggingExample({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ArcaneLoggingExample> createState() => _ArcaneLoggingExampleState();
|
||||
}
|
||||
|
||||
class _ArcaneLoggingExampleState extends State<ArcaneLoggingExample> {
|
||||
static const String _demoMetadataKey = "demo";
|
||||
static const String _demoMetadataValue =
|
||||
"This message will be included in all log messages.";
|
||||
|
||||
// Set up a subscriber that we can use to listen to logs in realtime.
|
||||
// Note: this is completely optional and does _not_ impact whether logs are
|
||||
// sent to any registered logging interfaces.
|
||||
late final StreamSubscription<String> _logStreamSubscriber;
|
||||
|
||||
// Used to collect the logs from the stream.
|
||||
final List<String> latestLogs = [];
|
||||
bool _persistentMetadataEnabled = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Listens to the Arcane logger stream of logs and adds them to the latestLogs list.
|
||||
_logStreamSubscriber = Arcane.logger.logStream.listen((message) {
|
||||
// If [Feature.logging] is disabled, we won't add the logs to the list or trigger
|
||||
// a rebuild.
|
||||
if (Feature.logging.enabled) {
|
||||
setState(() {
|
||||
latestLogs.insert(0, message);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Don't forget to properly dispose of the subscriber
|
||||
unawaited(_logStreamSubscriber.cancel());
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: Arcane.features.notifier,
|
||||
builder: (context, enabledFeatures, _) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Card(
|
||||
child: SizedBox(
|
||||
height: MediaQuery.sizeOf(context).height / 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Logging",
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
const Text("Include persistent demo metadata"),
|
||||
Switch(
|
||||
value: _persistentMetadataEnabled,
|
||||
onChanged: Feature.logging.disabled
|
||||
? null
|
||||
: (enabled) {
|
||||
setState(() {
|
||||
_persistentMetadataEnabled = enabled;
|
||||
});
|
||||
|
||||
if (enabled) {
|
||||
Arcane.logger.addPersistentMetadata({
|
||||
_demoMetadataKey: _demoMetadataValue,
|
||||
});
|
||||
} else {
|
||||
Arcane.logger.removePersistentMetadata(
|
||||
_demoMetadataKey,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
if (latestLogs.isEmpty)
|
||||
Text(
|
||||
"Log messages will appear here",
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
if (Feature.logging.disabled)
|
||||
Text(
|
||||
"Logging feature is disabled.",
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: latestLogs.length,
|
||||
itemBuilder: (context, index) {
|
||||
return Text(latestLogs[index]);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// * Authentication
|
||||
// Arcane's authentication system provides a simple, standard interface
|
||||
// for common authentication tasks - including registration and account
|
||||
// management, logging in and out, etc. Authentication status is reflected
|
||||
// in realtime within the application as changes happen, so you can focus
|
||||
// on what's most important.
|
||||
class ArcaneAuthExample extends StatelessWidget {
|
||||
const ArcaneAuthExample({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: Arcane.features.notifier,
|
||||
builder: (context, enabledFeatures, _) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: Arcane.auth.isSignedIn,
|
||||
builder: (context, isSignedIn, _) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
"Authentication",
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: Feature.authentication.enabled
|
||||
? () async {
|
||||
if (isSignedIn) {
|
||||
await Arcane.auth.logOut();
|
||||
} else {
|
||||
await Arcane.auth.login<Credentials>(
|
||||
input: (
|
||||
email: "email",
|
||||
password: "password",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: Text(
|
||||
isSignedIn ? "Sign out" : "Sign in",
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Text("Status: ${Arcane.auth.status.name}"),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// * Theme
|
||||
// at any time between light mode and dark mode, or set to follow the
|
||||
// system theme. In addition, themes can be swapped out on-the-fly,
|
||||
// enabling dynamic customizations and remote theme fetching.
|
||||
class ArcaneThemeExample extends StatelessWidget {
|
||||
const ArcaneThemeExample({
|
||||
super.key,
|
||||
});
|
||||
|
||||
static final Listenable themeListenable =
|
||||
Listenable.merge([Arcane.theme.darkTheme, Arcane.theme.lightTheme]);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
"Theme",
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Switch(
|
||||
value: context.isDarkMode,
|
||||
thumbIcon: WidgetStateProperty.resolveWith((states) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return const Icon(Icons.dark_mode);
|
||||
}
|
||||
return const Icon(Icons.light_mode);
|
||||
}),
|
||||
onChanged: (_) {
|
||||
// Always disable system mode and flip to the opposite of the effective mode
|
||||
Arcane.theme.switchTheme(
|
||||
themeMode:
|
||||
context.isDarkMode ? ThemeMode.light : ThemeMode.dark,
|
||||
);
|
||||
Arcane.log(
|
||||
"Switching theme",
|
||||
metadata: {
|
||||
"followingSystemTheme":
|
||||
"${Arcane.theme.isFollowingSystemTheme}",
|
||||
"newMode": Arcane.theme.currentThemeMode.name,
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Checkbox(
|
||||
value: Arcane.theme.isFollowingSystemTheme,
|
||||
onChanged: (value) {
|
||||
if (value == true) {
|
||||
Arcane.theme.followSystemTheme(context);
|
||||
} else {
|
||||
// When unchecking, set mode to the effective mode (not system)
|
||||
final ThemeMode effective =
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? ThemeMode.dark
|
||||
: ThemeMode.light;
|
||||
Arcane.theme.switchTheme(themeMode: effective);
|
||||
}
|
||||
Arcane.log(
|
||||
"Switching theme",
|
||||
metadata: {
|
||||
"followingSystemTheme":
|
||||
"${Arcane.theme.isFollowingSystemTheme}",
|
||||
"newMode": Arcane.theme.currentThemeMode.name,
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
const Text("Follow system"),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
"The current theme mode is ${Arcane.theme.currentModeOf(context).name} and "
|
||||
"is ${Arcane.theme.isFollowingSystemTheme ? "" : "not "}"
|
||||
"following the system theme.",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// * Feature flags
|
||||
// Arcane's feature flag system is extremely simple and flexible to use.
|
||||
// By registering _any_ enum (or even multiple enums!), features can be
|
||||
// toggled on and off at any point. The feature flag system even offers
|
||||
// a notifier, stream, and app-level scope so you can react to changes as
|
||||
// they happen. Fetch your remote config and use it to dynamically enable
|
||||
// and disable features with ease!
|
||||
class ArcaneFeatureFlagsExample extends StatelessWidget {
|
||||
const ArcaneFeatureFlagsExample({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ArcaneFeatureFlagProvider flags = context.featureFlags;
|
||||
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
"Feature Flags",
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: Feature.values.length,
|
||||
itemBuilder: (context, i) {
|
||||
final Feature feature = Feature.values[i];
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(feature.name),
|
||||
Switch(
|
||||
value: flags.isEnabled(feature),
|
||||
onChanged: (_) {
|
||||
flags.isEnabled(feature)
|
||||
? flags.disableFeature(feature)
|
||||
: flags.enableFeature(feature);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// * Environment
|
||||
// Quickly and easily toggle between a "normal" and "debug" environment
|
||||
// within your application. This is particularly useful during development
|
||||
// when you may want to change the behavior of the application under
|
||||
// certain conditions.
|
||||
class ArcaneEnvironmentExample extends StatelessWidget {
|
||||
static const Environment stagingEnvironment = Environment("staging");
|
||||
|
||||
const ArcaneEnvironmentExample({
|
||||
super.key,
|
||||
});
|
||||
|
||||
Environment _nextEnvironment(Environment current) {
|
||||
if (current == Environment.normal) return Environment.debug;
|
||||
if (current == Environment.debug) return stagingEnvironment;
|
||||
return Environment.normal;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
"Environment",
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
final ArcaneEnvironmentService environment = Arcane.environment;
|
||||
final Environment previousEnvironment = environment.current;
|
||||
final Environment nextEnvironment = _nextEnvironment(
|
||||
previousEnvironment,
|
||||
);
|
||||
|
||||
environment.setEnvironment(nextEnvironment);
|
||||
|
||||
Arcane.log(
|
||||
"Environment changed.",
|
||||
metadata: {
|
||||
"previous": previousEnvironment.name,
|
||||
"current": nextEnvironment.name,
|
||||
},
|
||||
);
|
||||
},
|
||||
child: const Text("Cycle environment"),
|
||||
),
|
||||
ValueListenableBuilder<Environment>(
|
||||
valueListenable: Arcane.environment.notifier,
|
||||
builder: (context, environment, _) {
|
||||
return Text(
|
||||
"Environment: ${environment.name}",
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// * Services
|
||||
/// Arcane's services system is flexible and minimal, leaving the power
|
||||
/// and control in developers' hands. This system powers much of Arcane
|
||||
/// internally, so you know it's reliable.
|
||||
class ArcaneServicesExample extends StatelessWidget {
|
||||
const ArcaneServicesExample({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ArcaneServiceProvider serviceProvider = ArcaneServiceProvider.of(
|
||||
context,
|
||||
);
|
||||
|
||||
return ValueListenableBuilder<List<ArcaneService>>(
|
||||
valueListenable: serviceProvider.notifier!,
|
||||
builder: (context, _, __) {
|
||||
final FavoriteColorService? service =
|
||||
ArcaneServiceProvider.serviceOfType<FavoriteColorService>(context);
|
||||
|
||||
Widget buildCard(MaterialColor? color) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
"Services",
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
Text(
|
||||
color != null ? "Favorite color: ${color.name}" : "",
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
if (service == null) {
|
||||
final FavoriteColorService nextService =
|
||||
FavoriteColorService()
|
||||
..syncFromCurrentTheme(colors);
|
||||
|
||||
serviceProvider.addService(
|
||||
nextService,
|
||||
);
|
||||
|
||||
Arcane.log(
|
||||
"Service registered.",
|
||||
metadata: {"service": "FavoriteColorService"},
|
||||
);
|
||||
} else {
|
||||
serviceProvider.removeService<FavoriteColorService>();
|
||||
|
||||
Arcane.log(
|
||||
"Service removed.",
|
||||
metadata: {"service": "FavoriteColorService"},
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
'${service == null ? 'Register' : 'Remove'} service',
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
spacing: 8,
|
||||
children: [
|
||||
const Text("Color"),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
itemCount: colors.length,
|
||||
scrollDirection: Axis.horizontal,
|
||||
separatorBuilder: (_, __) =>
|
||||
const SizedBox(width: 4),
|
||||
itemBuilder: (context, index) {
|
||||
return Opacity(
|
||||
opacity: service == null ? 0.4 : 1,
|
||||
child: InkWell(
|
||||
onTap: service == null
|
||||
? null
|
||||
: () {
|
||||
service.setMyFavoriteColor(
|
||||
colors[index],
|
||||
);
|
||||
Arcane.log(
|
||||
"Set a color in FavoriteColorService",
|
||||
metadata: {
|
||||
"color": colors[index].name ??
|
||||
"Unknown",
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: colors[index],
|
||||
border: color == colors[index]
|
||||
? Border.all(width: 2)
|
||||
: null,
|
||||
),
|
||||
width: 20,
|
||||
height: 20,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"Service is ${service != null ? "" : "not "}registered",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (service == null) return buildCard(null);
|
||||
|
||||
return ValueListenableBuilder<MaterialColor?>(
|
||||
valueListenable: service.notifier,
|
||||
builder: (context, color, _) => buildCard(color),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
import "package:flutter/foundation.dart";
|
||||
import "package:uuid/uuid.dart";
|
||||
|
||||
class IdService extends ArcaneService {
|
||||
static final IdService _instance = IdService._internal();
|
||||
static IdService get I => _instance;
|
||||
|
||||
IdService._internal();
|
||||
|
||||
bool _initialized = false;
|
||||
bool get initialized => I._initialized;
|
||||
|
||||
String? _sessionId;
|
||||
ValueListenable<String?> get sessionId =>
|
||||
ValueNotifier<String?>(I._sessionId);
|
||||
|
||||
String get newId => uuid.v7();
|
||||
|
||||
/// The `Uuid` instance used for generating unique IDs.
|
||||
static const Uuid uuid = Uuid();
|
||||
|
||||
Future<void> init() async {
|
||||
Arcane.log(
|
||||
"Initializing ID Service",
|
||||
level: Level.debug,
|
||||
);
|
||||
|
||||
I._sessionId = uuid.v7();
|
||||
I._initialized = true;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class FavoriteColorService extends ArcaneService {
|
||||
FavoriteColorService();
|
||||
|
||||
MaterialColor? get myFavoriteColor => _notifier.value;
|
||||
|
||||
final ValueNotifier<MaterialColor?> _notifier =
|
||||
ValueNotifier<MaterialColor?>(null);
|
||||
|
||||
ValueNotifier<MaterialColor?> get notifier => _notifier;
|
||||
|
||||
void setMyFavoriteColor(MaterialColor? newValue) {
|
||||
if (_notifier.value == newValue) return;
|
||||
|
||||
_notifier.value = newValue;
|
||||
|
||||
if (newValue == null) return;
|
||||
|
||||
// Apply the seed to both themes so switching mode keeps the same color
|
||||
// family.
|
||||
Arcane.theme.setLightTheme(
|
||||
ThemeData(
|
||||
brightness: Brightness.light,
|
||||
colorSchemeSeed: newValue,
|
||||
),
|
||||
);
|
||||
|
||||
Arcane.theme.dark = ThemeData(
|
||||
brightness: Brightness.dark,
|
||||
colorSchemeSeed: newValue,
|
||||
);
|
||||
}
|
||||
|
||||
void syncFromCurrentTheme(Iterable<MaterialColor> palette) {
|
||||
final Iterator<MaterialColor> iterator = palette.iterator;
|
||||
if (!iterator.moveNext()) {
|
||||
_notifier.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
final Color target = Arcane.theme.currentTheme.colorScheme.primary;
|
||||
MaterialColor closest = iterator.current;
|
||||
double closestDistance = _colorDistanceSquared(closest, target);
|
||||
|
||||
while (iterator.moveNext()) {
|
||||
final MaterialColor candidate = iterator.current;
|
||||
final double distance = _colorDistanceSquared(candidate, target);
|
||||
if (distance < closestDistance) {
|
||||
closest = candidate;
|
||||
closestDistance = distance;
|
||||
}
|
||||
}
|
||||
|
||||
if (_notifier.value != closest) {
|
||||
_notifier.value = closest;
|
||||
}
|
||||
}
|
||||
|
||||
double _colorDistanceSquared(Color a, Color b) {
|
||||
final double dr = a.r - b.r;
|
||||
final double dg = a.g - b.g;
|
||||
final double db = a.b - b.b;
|
||||
return (dr * dr) + (dg * dg) + (db * db);
|
||||
}
|
||||
}
|
||||
|
||||
extension MaterialColorName on MaterialColor {
|
||||
String? get name {
|
||||
final double red = double.parse(r.toStringAsFixed(4));
|
||||
final double green = double.parse(g.toStringAsFixed(4));
|
||||
final double blue = double.parse(b.toStringAsFixed(4));
|
||||
if (red == 0.9569 && green == 0.2627 && blue == 0.2118) return "red";
|
||||
if (red == 1 && green == 0.5961 && blue == 0) return "orange";
|
||||
if (red == 1 && green == 0.9216 && blue == 0.2314) return "yellow";
|
||||
if (red == 0.2980 && green == 0.6863 && blue == 0.3137) return "green";
|
||||
if (red == 0.1294 && green == 0.5882 && blue == 0.9529) return "blue";
|
||||
if (red == 0.6118 && green == 0.1529 && blue == 0.6902) return "indigo";
|
||||
if (red == 0.4039 && green == 0.2275 && blue == 0.7176) return "violet";
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
extension ColorName on Color {
|
||||
String? get name {
|
||||
final double red = double.parse(r.toStringAsFixed(4));
|
||||
final double green = double.parse(g.toStringAsFixed(4));
|
||||
final double blue = double.parse(b.toStringAsFixed(4));
|
||||
|
||||
if (red == 0.5647 && green == 0.2902 && blue == 0.2588) return "red";
|
||||
if (red == 0.5216 && green == 0.3255 && blue == 0.0941) return "orange";
|
||||
if (red == 0.4078 && green == 0.3725 && blue == 0.0706) return "yellow";
|
||||
if (red == 0.2314 && green == 0.4118 && blue == 0.2235) return "green";
|
||||
if (red == 0.2118 && green == 0.3804 && blue == 0.5569) return "blue";
|
||||
if (red == 0.4824 && green == 0.3059 && blue == 0.498) return "indigo";
|
||||
if (red == 0.4078 && green == 0.3294 && blue == 0.5569) return "violet";
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
final ThemeData darkTheme = ThemeData.dark();
|
||||
final ThemeData lightTheme = ThemeData.light();
|
||||
const MaterialColor defaultSeedColor = Colors.blue;
|
||||
|
||||
final ThemeData darkTheme = ThemeData(
|
||||
brightness: Brightness.dark,
|
||||
colorSchemeSeed: defaultSeedColor,
|
||||
);
|
||||
|
||||
final ThemeData lightTheme = ThemeData(
|
||||
brightness: Brightness.light,
|
||||
colorSchemeSeed: defaultSeedColor,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user