complete project
51
.gitignore
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.packages
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
# Rust related
|
||||
.cargo/
|
||||
target/
|
||||
|
||||
# Generated messages
|
||||
*/**/messages/
|
6
.gitmodules
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
[submodule "packages/real_time_chart"]
|
||||
path = packages/real_time_chart
|
||||
url = https://avoid.sh/bachelor/real_time_chart.git
|
||||
[submodule "packages/virtual_keyboard_multi_language"]
|
||||
path = packages/virtual_keyboard_multi_language
|
||||
url = https://avoid.sh/bachelor/virtual_keyboard_multi_language.git
|
33
.metadata
Normal file
@ -0,0 +1,33 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "7482962148e8d758338d8a28f589f317e1e42ba4"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 7482962148e8d758338d8a28f589f317e1e42ba4
|
||||
base_revision: 7482962148e8d758338d8a28f589f317e1e42ba4
|
||||
- platform: linux
|
||||
create_revision: 7482962148e8d758338d8a28f589f317e1e42ba4
|
||||
base_revision: 7482962148e8d758338d8a28f589f317e1e42ba4
|
||||
- platform: macos
|
||||
create_revision: 7482962148e8d758338d8a28f589f317e1e42ba4
|
||||
base_revision: 7482962148e8d758338d8a28f589f317e1e42ba4
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
3
.puro.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"env": "bachelor_elinux"
|
||||
}
|
19
.vscode/c_cpp_properties.json
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Mac",
|
||||
"includePath": [
|
||||
"${workspaceFolder}/**"
|
||||
],
|
||||
"defines": [],
|
||||
"macFrameworkPath": [
|
||||
"/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks"
|
||||
],
|
||||
"compilerPath": "/usr/bin/clang",
|
||||
"cStandard": "c17",
|
||||
"cppStandard": "c++17",
|
||||
"intelliSenseMode": "macos-clang-arm64"
|
||||
}
|
||||
],
|
||||
"version": 4
|
||||
}
|
4
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"dart.flutterSdkPath": "/Users/fabian/.puro/envs/bachelor_elinux/flutter",
|
||||
"dart.sdkPath": "/Users/fabian/.puro/envs/bachelor_elinux/flutter/bin/cache/dart-sdk"
|
||||
}
|
16
.vscode/tasks.json
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "make",
|
||||
"type": "shell",
|
||||
"command": "dart run build_runner build --delete-conflicting-outputs",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
29
LICENSE
Normal file
@ -0,0 +1,29 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2023, Fabian Baldeau
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
17
Makefile
Normal file
@ -0,0 +1,17 @@
|
||||
release:
|
||||
flutter pub global run flutterpi_tool build --arch=arm64 --cpu=pi4 --release
|
||||
|
||||
profile:
|
||||
flutter pub global run flutterpi_tool build --arch=arm64 --cpu=pi4 --profile
|
||||
|
||||
sync-release:
|
||||
rsync -a ./build/flutter_assets/ fabian@raspberrypi:/home/fabian/release
|
||||
|
||||
sync-profile:
|
||||
rsync -a ./build/flutter_assets/ fabian@raspberrypi:/home/fabian/profile
|
||||
|
||||
stop:
|
||||
ssh -o BatchMode=no fabian@raspberrypi pkill flutter-pi
|
||||
|
||||
dep-graph:
|
||||
layerlens
|
16
README.md
Normal file
@ -0,0 +1,16 @@
|
||||
# flutter_elinux
|
||||
|
||||
A demo Flutter project for embedded Linux systems.
|
||||
|
||||
## Getting Started
|
||||
|
||||
This project is a starting point for a Flutter application.
|
||||
|
||||
A few resources to get you started if this is your first Flutter project:
|
||||
|
||||
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||
|
||||
For help getting started with Flutter development, view the
|
||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
32
analysis_options.yaml
Normal file
@ -0,0 +1,32 @@
|
||||
# This file configures the analyzer, which statically analyzes Dart code to
|
||||
# check for errors, warnings, and lints.
|
||||
#
|
||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||
# invoked from the command line by running `flutter analyze`.
|
||||
|
||||
# The following line activates a set of recommended lints for Flutter apps,
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
# included above or to enable additional rules. A list of all available lints
|
||||
# and their documentation is published at https://dart.dev/lints.
|
||||
#
|
||||
# Instead of disabling a lint rule for the entire project in the
|
||||
# section below, it can also be suppressed for a single line of code
|
||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||
# producing the lint.
|
||||
rules:
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
|
||||
analyzer:
|
||||
plugins:
|
||||
- custom_lint
|
BIN
assets/images/flame_benchmark/rogue_shooter/bullet.png
Normal file
After Width: | Height: | Size: 343 B |
BIN
assets/images/flame_benchmark/rogue_shooter/enemy.png
Normal file
After Width: | Height: | Size: 389 B |
BIN
assets/images/flame_benchmark/rogue_shooter/explosion.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
assets/images/flame_benchmark/rogue_shooter/player.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
assets/images/flame_benchmark/rogue_shooter/stars.png
Normal file
After Width: | Height: | Size: 332 B |
BIN
assets/images/flame_benchmark/sprite_benchmark/ember.png
Normal file
After Width: | Height: | Size: 851 B |
BIN
assets/img/image_placeholder.png
Normal file
After Width: | Height: | Size: 8.0 KiB |
BIN
assets/img/raspi_logo.png
Normal file
After Width: | Height: | Size: 99 KiB |
BIN
assets/img/tagesschau_logo.jpg
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
assets/rive/dark_light_switch.riv
Normal file
BIN
assets/rive/walk_cycle_blend.riv
Normal file
BIN
assets/rive/weather_icon_animation.riv
Normal file
BIN
assets/video/video_1080p.mp4
Normal file
BIN
assets/video/video_2160p.mp4
Normal file
BIN
assets/video/video_720p.mp4
Normal file
36
lib/app.dart
Normal file
@ -0,0 +1,36 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'common/settings/app_settings.dart';
|
||||
import 'constants/colors.dart';
|
||||
import 'common/routing/router.dart';
|
||||
|
||||
class App extends ConsumerWidget {
|
||||
const App({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
bool useMaterial3 = ref.watch(useMaterial3Provider);
|
||||
bool useDarkMode = ref.watch(useDarkModeProvider);
|
||||
bool showPerformanceOverlay = ref.watch(showPerformanceOverlayProvider);
|
||||
ColorSeed colorSeedSelected = ref.watch(colorSeedSelectedProvider);
|
||||
|
||||
return MaterialApp.router(
|
||||
showPerformanceOverlay: showPerformanceOverlay,
|
||||
routerConfig: goRouter,
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: 'eLinux',
|
||||
theme: ThemeData(
|
||||
colorSchemeSeed: colorSeedSelected.color,
|
||||
useMaterial3: useMaterial3,
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
colorSchemeSeed: colorSeedSelected.color,
|
||||
useMaterial3: useMaterial3,
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
themeMode: useDarkMode ? ThemeMode.dark : ThemeMode.light,
|
||||
);
|
||||
}
|
||||
}
|
3
lib/common/extension/getCharCodes.dart
Normal file
@ -0,0 +1,3 @@
|
||||
extension StringExtension on String {
|
||||
Iterable<int> getCharCodes() => codeUnits;
|
||||
}
|
11
lib/common/extension/toFormatedNewsDate.dart
Normal file
@ -0,0 +1,11 @@
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
extension StringExtension on String {
|
||||
String toFormattedNewsDate() {
|
||||
final parsedDate = DateTime.parse(this);
|
||||
|
||||
// The intl package is used here for more robust formatting options
|
||||
final DateFormat formatter = DateFormat('dd.MM.yyyy HH:mm', 'de_DE');
|
||||
return "Stand: ${formatter.format(parsedDate)} Uhr";
|
||||
}
|
||||
}
|
203
lib/common/routing/router.dart
Normal file
@ -0,0 +1,203 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:nm/nm.dart';
|
||||
|
||||
import '../../features/benchmark/presentation/benchmark_screen.dart';
|
||||
import '../../features/benchmark/presentation/map_benchmark_screen.dart';
|
||||
import '../../features/benchmark/presentation/rogue_shooter_screen.dart';
|
||||
import '../../features/benchmark/presentation/sprite_benchmark_screen.dart';
|
||||
import '../../features/benchmark/presentation/video_benchmark_screen.dart';
|
||||
import '../../features/benchmark/presentation/vsync_benchmark_screen.dart';
|
||||
import '../../features/home/presentation/home_screen.dart';
|
||||
import '../../features/material_demo/presentation/material_demo_screen.dart';
|
||||
import '../../features/matrix_rgb/presentation/matrix_screen.dart';
|
||||
import '../../features/morse_led/presentation/led_screen.dart';
|
||||
import '../../features/news_api/presentation/news_screen.dart';
|
||||
import '../../features/settings/presentation/settings_screen/settings_screen.dart';
|
||||
import '../../features/settings/presentation/wifi_settings_screen/accesspoint_screen.dart';
|
||||
import '../../features/settings/presentation/wifi_settings_screen/wifi_settings_screen.dart';
|
||||
import '../../features/system_resources/presentation/system_resources_screen.dart';
|
||||
import '../widgets/home_scaffold.dart';
|
||||
import 'routes.dart';
|
||||
|
||||
const ValueKey<String> _scaffoldKey = ValueKey<String>('App scaffold');
|
||||
|
||||
final goRouter = GoRouter(
|
||||
initialLocation: '/',
|
||||
debugLogDiagnostics: false,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/',
|
||||
redirect: (_, __) => '/home',
|
||||
),
|
||||
GoRoute(
|
||||
path: '/home',
|
||||
name: RoutesHome.home.name,
|
||||
pageBuilder: (BuildContext context, GoRouterState state) =>
|
||||
FadeTransitionPage(
|
||||
key: _scaffoldKey,
|
||||
child: const HomeScaffold(
|
||||
selectedTab: RoutesHome.home,
|
||||
child: HomeScreen(),
|
||||
),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/material-demo',
|
||||
name: RoutesHome.materialDemo.name,
|
||||
pageBuilder: (BuildContext context, GoRouterState state) =>
|
||||
FadeTransitionPage(
|
||||
key: _scaffoldKey,
|
||||
child: const HomeScaffold(
|
||||
selectedTab: RoutesHome.materialDemo,
|
||||
child: MaterialDemoScreen(),
|
||||
),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/news',
|
||||
name: RoutesHome.news.name,
|
||||
pageBuilder: (BuildContext context, GoRouterState state) =>
|
||||
FadeTransitionPage(
|
||||
key: _scaffoldKey,
|
||||
child: const HomeScaffold(
|
||||
selectedTab: RoutesHome.news,
|
||||
child: NewsScreen(),
|
||||
),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/matrix',
|
||||
name: RoutesHome.matrix.name,
|
||||
pageBuilder: (BuildContext context, GoRouterState state) =>
|
||||
FadeTransitionPage(
|
||||
key: _scaffoldKey,
|
||||
child: const HomeScaffold(
|
||||
selectedTab: RoutesHome.matrix,
|
||||
child: MatrixScreen(),
|
||||
),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/led',
|
||||
name: RoutesHome.led.name,
|
||||
pageBuilder: (BuildContext context, GoRouterState state) =>
|
||||
FadeTransitionPage(
|
||||
key: _scaffoldKey,
|
||||
child: const HomeScaffold(
|
||||
selectedTab: RoutesHome.led,
|
||||
child: LedScreen(),
|
||||
),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/system',
|
||||
name: RoutesHome.system.name,
|
||||
pageBuilder: (BuildContext context, GoRouterState state) =>
|
||||
FadeTransitionPage(
|
||||
key: _scaffoldKey,
|
||||
child: const HomeScaffold(
|
||||
selectedTab: RoutesHome.system,
|
||||
child: SystemResourcesScreen(),
|
||||
),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/benchmark',
|
||||
name: RoutesHome.benchmark.name,
|
||||
pageBuilder: (BuildContext context, GoRouterState state) =>
|
||||
FadeTransitionPage(
|
||||
key: _scaffoldKey,
|
||||
child: const HomeScaffold(
|
||||
selectedTab: RoutesHome.benchmark,
|
||||
child: BenchmarkScreen(),
|
||||
),
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: RoutesBenchmark.sprites.name,
|
||||
name: RoutesBenchmark.sprites.name,
|
||||
builder: (context, state) => const SpriteBenchmarkScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: RoutesBenchmark.rogueShooter.name,
|
||||
name: RoutesBenchmark.rogueShooter.name,
|
||||
builder: (context, state) => const RogueShooterScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: RoutesBenchmark.shader.name,
|
||||
name: RoutesBenchmark.shader.name,
|
||||
builder: (context, state) => const VsyncBenchmarkScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: RoutesBenchmark.video.name,
|
||||
name: RoutesBenchmark.video.name,
|
||||
builder: (context, state) => const VideoBenchmarkScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: RoutesBenchmark.map.name,
|
||||
name: RoutesBenchmark.map.name,
|
||||
builder: (context, state) => const MapBenchmarkScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/settings',
|
||||
name: RoutesHome.settings.name,
|
||||
pageBuilder: (BuildContext context, GoRouterState state) =>
|
||||
FadeTransitionPage(
|
||||
key: _scaffoldKey,
|
||||
child: const HomeScaffold(
|
||||
selectedTab: RoutesHome.settings,
|
||||
child: SettingsScreen(),
|
||||
),
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: RoutesSettings.wifi.name,
|
||||
name: RoutesSettings.wifi.name,
|
||||
builder: (context, state) => const WifiSettingsScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'accesspoint',
|
||||
name: RoutesSettings.accesspoint.name,
|
||||
builder: (context, state) {
|
||||
if (state.extra == null) {
|
||||
return const AccessPointScreen();
|
||||
}
|
||||
final accessPoint = state.extra as NetworkManagerAccessPoint;
|
||||
return AccessPointScreen(
|
||||
accessPoint: accessPoint,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
/// A page that fades in and out.
|
||||
/// From the official Go Router Example:
|
||||
/// https://github.com/flutter/packages/blob/6701c9e618041574cf4cc53cb028a38de8322b53/packages/go_router/example/lib/books/main.dart#L154
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
class FadeTransitionPage extends CustomTransitionPage<void> {
|
||||
/// Creates a [FadeTransitionPage].
|
||||
FadeTransitionPage({
|
||||
required LocalKey super.key,
|
||||
required super.child,
|
||||
}) : super(
|
||||
transitionsBuilder: (BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
Widget child) =>
|
||||
FadeTransition(
|
||||
opacity: animation.drive(_curveTween),
|
||||
child: child,
|
||||
));
|
||||
|
||||
static final CurveTween _curveTween = CurveTween(curve: Curves.easeIn);
|
||||
}
|
29
lib/common/routing/routes.dart
Normal file
@ -0,0 +1,29 @@
|
||||
enum RoutesHome {
|
||||
home,
|
||||
materialDemo,
|
||||
benchmark,
|
||||
news,
|
||||
led,
|
||||
matrix,
|
||||
system,
|
||||
settings,
|
||||
}
|
||||
|
||||
enum RoutesSettings {
|
||||
wifi,
|
||||
accesspoint,
|
||||
}
|
||||
|
||||
enum RoutesBenchmark {
|
||||
sprites,
|
||||
rogueShooter,
|
||||
shader,
|
||||
video,
|
||||
map,
|
||||
}
|
||||
|
||||
enum RoutesEndlessRunner {
|
||||
play,
|
||||
session,
|
||||
settings,
|
||||
}
|
61
lib/common/settings/app_settings.dart
Normal file
@ -0,0 +1,61 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../../constants/colors.dart';
|
||||
import 'sharedPrefs.dart';
|
||||
|
||||
part 'app_settings.g.dart';
|
||||
|
||||
@riverpod
|
||||
class UseMaterial3 extends _$UseMaterial3 {
|
||||
@override
|
||||
bool build() {
|
||||
final bool material3 = SharedPrefs().getValue('material3') ?? true;
|
||||
return material3;
|
||||
}
|
||||
|
||||
void toggle() {
|
||||
SharedPrefs().setValue('material3', !state);
|
||||
state = !state;
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class UseDarkMode extends _$UseDarkMode {
|
||||
@override
|
||||
bool build() {
|
||||
final bool darkMode = SharedPrefs().getValue('darkMode') ?? false;
|
||||
return darkMode;
|
||||
}
|
||||
|
||||
void toggle() {
|
||||
SharedPrefs().setValue('darkMode', !state);
|
||||
state = !state;
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class ColorSeedSelected extends _$ColorSeedSelected {
|
||||
@override
|
||||
ColorSeed build() {
|
||||
final String colorSeed =
|
||||
SharedPrefs().getValue('colorSeed') ?? ColorSeed.baseColor.label;
|
||||
return ColorSeed.values.firstWhere((element) => element.label == colorSeed);
|
||||
}
|
||||
|
||||
void setColorSeed(ColorSeed colorSeed) {
|
||||
SharedPrefs().setValue('colorSeed', colorSeed.label);
|
||||
state = colorSeed;
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class ShowPerformanceOverlay extends _$ShowPerformanceOverlay {
|
||||
@override
|
||||
bool build() {
|
||||
return false;
|
||||
}
|
||||
|
||||
void toggle() {
|
||||
state = !state;
|
||||
}
|
||||
}
|
73
lib/common/settings/app_settings.g.dart
Normal file
@ -0,0 +1,73 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'app_settings.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$useMaterial3Hash() => r'38dbfd0b19379fa8ee73c1b0ec03dfe914dc23bc';
|
||||
|
||||
/// See also [UseMaterial3].
|
||||
@ProviderFor(UseMaterial3)
|
||||
final useMaterial3Provider =
|
||||
AutoDisposeNotifierProvider<UseMaterial3, bool>.internal(
|
||||
UseMaterial3.new,
|
||||
name: r'useMaterial3Provider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$useMaterial3Hash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$UseMaterial3 = AutoDisposeNotifier<bool>;
|
||||
String _$useDarkModeHash() => r'd9f1ffc5d34dc2c9a59b99e0393b8f5ccc0a7040';
|
||||
|
||||
/// See also [UseDarkMode].
|
||||
@ProviderFor(UseDarkMode)
|
||||
final useDarkModeProvider =
|
||||
AutoDisposeNotifierProvider<UseDarkMode, bool>.internal(
|
||||
UseDarkMode.new,
|
||||
name: r'useDarkModeProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$useDarkModeHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$UseDarkMode = AutoDisposeNotifier<bool>;
|
||||
String _$colorSeedSelectedHash() => r'13745dbe8aee72eb6d43f49cb0d155164a96eb8c';
|
||||
|
||||
/// See also [ColorSeedSelected].
|
||||
@ProviderFor(ColorSeedSelected)
|
||||
final colorSeedSelectedProvider =
|
||||
AutoDisposeNotifierProvider<ColorSeedSelected, ColorSeed>.internal(
|
||||
ColorSeedSelected.new,
|
||||
name: r'colorSeedSelectedProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$colorSeedSelectedHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$ColorSeedSelected = AutoDisposeNotifier<ColorSeed>;
|
||||
String _$showPerformanceOverlayHash() =>
|
||||
r'8fb9b73ed0c32730b2b83cf0e6db78f066603ac4';
|
||||
|
||||
/// See also [ShowPerformanceOverlay].
|
||||
@ProviderFor(ShowPerformanceOverlay)
|
||||
final showPerformanceOverlayProvider =
|
||||
AutoDisposeNotifierProvider<ShowPerformanceOverlay, bool>.internal(
|
||||
ShowPerformanceOverlay.new,
|
||||
name: r'showPerformanceOverlayProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$showPerformanceOverlayHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$ShowPerformanceOverlay = AutoDisposeNotifier<bool>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
88
lib/common/settings/custom_layout.dart
Normal file
@ -0,0 +1,88 @@
|
||||
import 'package:virtual_keyboard_multi_language/virtual_keyboard_multi_language.dart';
|
||||
|
||||
// example custom layout file (example from the virtual_keyboard_multi_language package)
|
||||
// here different layouts for different languages could be defined:
|
||||
|
||||
class CustomLayoutKeys extends VirtualKeyboardLayoutKeys {
|
||||
@override
|
||||
int getLanguagesCount() => 2;
|
||||
|
||||
@override
|
||||
List<List> getLanguage(int index) {
|
||||
switch (index) {
|
||||
case 1:
|
||||
return _arabicLayout;
|
||||
default:
|
||||
return defaultEnglishLayout;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const List<List> _arabicLayout = [
|
||||
// Row 1
|
||||
[
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
'4',
|
||||
'5',
|
||||
'6',
|
||||
'7',
|
||||
'8',
|
||||
'9',
|
||||
'0',
|
||||
],
|
||||
// Row 2
|
||||
[
|
||||
'ض',
|
||||
'ص',
|
||||
'ث',
|
||||
'ق',
|
||||
'ف',
|
||||
'غ',
|
||||
'ع',
|
||||
'ه',
|
||||
'خ',
|
||||
'ح',
|
||||
'د',
|
||||
VirtualKeyboardKeyAction.Backspace
|
||||
],
|
||||
// Row 3
|
||||
[
|
||||
'ش',
|
||||
'س',
|
||||
'ي',
|
||||
'ب',
|
||||
'ل',
|
||||
'ا',
|
||||
'ت',
|
||||
'ن',
|
||||
'م',
|
||||
'ك',
|
||||
'ط',
|
||||
VirtualKeyboardKeyAction.Return
|
||||
],
|
||||
// Row 4
|
||||
[
|
||||
'ذ',
|
||||
'ئ',
|
||||
'ء',
|
||||
'ؤ',
|
||||
'ر',
|
||||
'لا',
|
||||
'ى',
|
||||
'ة',
|
||||
'و',
|
||||
'.',
|
||||
'ظ',
|
||||
VirtualKeyboardKeyAction.Shift
|
||||
],
|
||||
// Row 5
|
||||
[
|
||||
VirtualKeyboardKeyAction.SwithLanguage,
|
||||
'@',
|
||||
VirtualKeyboardKeyAction.Space,
|
||||
'-',
|
||||
'_',
|
||||
]
|
||||
];
|
31
lib/common/settings/sharedPrefs.dart
Normal file
@ -0,0 +1,31 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// A class to handle shared preferences
|
||||
class SharedPrefs {
|
||||
late final SharedPreferences _sharedPrefs;
|
||||
|
||||
static final SharedPrefs _instance = SharedPrefs._internal();
|
||||
factory SharedPrefs() => _instance;
|
||||
SharedPrefs._internal();
|
||||
Future<void> init() async {
|
||||
_sharedPrefs = await SharedPreferences.getInstance();
|
||||
}
|
||||
|
||||
setValue(String key, dynamic value) {
|
||||
if (value is bool) {
|
||||
_sharedPrefs.setBool(key, value);
|
||||
} else if (value is int) {
|
||||
_sharedPrefs.setInt(key, value);
|
||||
} else if (value is double) {
|
||||
_sharedPrefs.setDouble(key, value);
|
||||
} else if (value is String) {
|
||||
_sharedPrefs.setString(key, value);
|
||||
} else if (value is List<String>) {
|
||||
_sharedPrefs.setStringList(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
dynamic getValue(String key) {
|
||||
return _sharedPrefs.get(key);
|
||||
}
|
||||
}
|
13
lib/common/utils/linux_shutdown.dart
Normal file
@ -0,0 +1,13 @@
|
||||
import 'dart:io';
|
||||
|
||||
Future<void> linuxShutdown({bool restart = false, int delay = 0}) {
|
||||
final params = <String>[];
|
||||
|
||||
if (restart) {
|
||||
params.add('-r');
|
||||
}
|
||||
|
||||
params.addAll(<String>['-t', delay.toString(), 'now']);
|
||||
|
||||
return Process.run('sudo', ['shutdown', ...params]);
|
||||
}
|
6
lib/common/utils/time_stream.dart
Normal file
@ -0,0 +1,6 @@
|
||||
Stream<DateTime> timeStream() async* {
|
||||
while (true) {
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
yield DateTime.now();
|
||||
}
|
||||
}
|
30
lib/common/widgets/clock.dart
Normal file
@ -0,0 +1,30 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../utils/time_stream.dart';
|
||||
|
||||
class ClockWidget extends ConsumerWidget {
|
||||
const ClockWidget({this.textStyle, super.key});
|
||||
|
||||
final TextStyle? textStyle;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return StreamBuilder<DateTime>(
|
||||
stream: timeStream(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
DateTime time = snapshot.data ?? DateTime.now();
|
||||
DateFormat format = DateFormat('HH:mm:ss');
|
||||
return Text(
|
||||
format.format(time),
|
||||
style: textStyle,
|
||||
);
|
||||
} else {
|
||||
return const CircularProgressIndicator();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
52
lib/common/widgets/floating_menu_button.dart
Normal file
@ -0,0 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class FloatingMenuButton extends StatelessWidget {
|
||||
const FloatingMenuButton({this.popupMenuButton, super.key});
|
||||
|
||||
final PopupMenuButton? popupMenuButton;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PositionedDirectional(
|
||||
start: 16,
|
||||
top: 16,
|
||||
child: SafeArea(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
// check if dark mode
|
||||
color: (Theme.of(context).brightness == Brightness.light)
|
||||
? Colors.grey.withOpacity(0.5)
|
||||
: Colors.black.withOpacity(0.5),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 7,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () => context.pop(),
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
),
|
||||
if (popupMenuButton != null)
|
||||
Row(
|
||||
children: [
|
||||
const SizedBox(width: 8),
|
||||
popupMenuButton!,
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
100
lib/common/widgets/home_scaffold.dart
Normal file
@ -0,0 +1,100 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../routing/routes.dart';
|
||||
import 'scaffold_with_statusbar.dart';
|
||||
|
||||
/// The Scaffold that is used for the home screen.
|
||||
class HomeScaffold extends StatelessWidget {
|
||||
const HomeScaffold({
|
||||
required this.selectedTab,
|
||||
required this.child,
|
||||
this.appBar,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// Which tab of the scaffold to display.
|
||||
final RoutesHome selectedTab;
|
||||
|
||||
/// The scaffold body.
|
||||
final Widget child;
|
||||
|
||||
/// The scaffold AppBar.
|
||||
final AppBar? appBar;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ScaffoldWithStatusbar(
|
||||
body: AdaptiveScaffold(
|
||||
appBar: appBar,
|
||||
smallBreakpoint: const WidthPlatformBreakpoint(end: 700),
|
||||
mediumBreakpoint:
|
||||
const WidthPlatformBreakpoint(begin: 700, end: 1400),
|
||||
largeBreakpoint: const WidthPlatformBreakpoint(begin: 1400),
|
||||
body: (_) => child,
|
||||
selectedIndex: selectedTab.index,
|
||||
destinations: const <NavigationDestination>[
|
||||
NavigationDestination(
|
||||
label: 'Home',
|
||||
icon: Icon(Icons.home_outlined),
|
||||
selectedIcon: Icon(Icons.home),
|
||||
),
|
||||
NavigationDestination(
|
||||
label: 'Material Demo',
|
||||
icon: Icon(Icons.widgets_outlined),
|
||||
selectedIcon: Icon(Icons.widgets),
|
||||
),
|
||||
NavigationDestination(
|
||||
label: 'Benchmark',
|
||||
icon: Icon(Icons.fireplace_outlined),
|
||||
selectedIcon: Icon(Icons.fireplace),
|
||||
),
|
||||
NavigationDestination(
|
||||
label: 'News',
|
||||
icon: Icon(Icons.newspaper_outlined),
|
||||
selectedIcon: Icon(Icons.newspaper),
|
||||
),
|
||||
NavigationDestination(
|
||||
label: 'Led',
|
||||
icon: Icon(Icons.fluorescent_outlined),
|
||||
selectedIcon: Icon(Icons.fluorescent),
|
||||
),
|
||||
NavigationDestination(
|
||||
label: 'Matrix',
|
||||
icon: Icon(Icons.grid_off),
|
||||
selectedIcon: Icon(Icons.grid_on),
|
||||
),
|
||||
NavigationDestination(
|
||||
label: 'System',
|
||||
icon: Icon(Icons.insert_chart_outlined_outlined),
|
||||
selectedIcon: Icon(Icons.insert_chart),
|
||||
),
|
||||
NavigationDestination(
|
||||
label: 'Settings',
|
||||
icon: Icon(Icons.settings_outlined),
|
||||
selectedIcon: Icon(Icons.settings),
|
||||
),
|
||||
],
|
||||
onSelectedIndexChange: (int idx) {
|
||||
switch (RoutesHome.values[idx]) {
|
||||
case RoutesHome.home:
|
||||
context.goNamed(RoutesHome.home.name);
|
||||
case RoutesHome.materialDemo:
|
||||
context.goNamed(RoutesHome.materialDemo.name);
|
||||
case RoutesHome.news:
|
||||
context.goNamed(RoutesHome.news.name);
|
||||
case RoutesHome.benchmark:
|
||||
context.goNamed(RoutesHome.benchmark.name);
|
||||
case RoutesHome.led:
|
||||
context.goNamed(RoutesHome.led.name);
|
||||
case RoutesHome.matrix:
|
||||
context.goNamed(RoutesHome.matrix.name);
|
||||
case RoutesHome.system:
|
||||
context.goNamed(RoutesHome.system.name);
|
||||
case RoutesHome.settings:
|
||||
context.goNamed(RoutesHome.settings.name);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
67
lib/common/widgets/menu_drawer.dart
Normal file
@ -0,0 +1,67 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../routing/routes.dart';
|
||||
|
||||
class MenuDrawer extends StatelessWidget {
|
||||
const MenuDrawer({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Drawer(
|
||||
child: ListView(
|
||||
children: <Widget>[
|
||||
const DrawerHeader(
|
||||
child: Column(
|
||||
children: [],
|
||||
)),
|
||||
ListTile(
|
||||
selected: GoRouterState.of(context).uri.toString() ==
|
||||
GoRouter.of(context).namedLocation(RoutesHome.home.name),
|
||||
title: const Text('Home'),
|
||||
leading: const Icon(Icons.home),
|
||||
onTap: () {
|
||||
context.replaceNamed(RoutesHome.home.name);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
selected: GoRouterState.of(context).uri.toString() ==
|
||||
GoRouter.of(context).namedLocation(RoutesHome.news.name),
|
||||
title: const Text('News'),
|
||||
leading: const Icon(Icons.newspaper),
|
||||
onTap: () {
|
||||
context.replaceNamed(RoutesHome.news.name);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
selected: GoRouterState.of(context).uri.toString() ==
|
||||
GoRouter.of(context).namedLocation(RoutesHome.news.name),
|
||||
title: const Text('Led'),
|
||||
leading: const Icon(Icons.newspaper),
|
||||
onTap: () {
|
||||
context.replaceNamed(RoutesHome.led.name);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
selected: GoRouterState.of(context).uri.toString() ==
|
||||
GoRouter.of(context).namedLocation(RoutesHome.news.name),
|
||||
title: const Text('Matrix'),
|
||||
leading: const Icon(Icons.newspaper),
|
||||
onTap: () {
|
||||
context.replaceNamed(RoutesHome.matrix.name);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
selected: GoRouterState.of(context).uri.toString() ==
|
||||
GoRouter.of(context).namedLocation(RoutesHome.settings.name),
|
||||
title: const Text('Settings'),
|
||||
leading: const Icon(Icons.settings),
|
||||
onTap: () {
|
||||
context.replaceNamed(RoutesHome.settings.name);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
34
lib/common/widgets/scaffold_with_statusbar.dart
Normal file
@ -0,0 +1,34 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'statusbar.dart';
|
||||
|
||||
class ScaffoldWithStatusbar extends StatelessWidget {
|
||||
const ScaffoldWithStatusbar({
|
||||
this.appBar,
|
||||
this.body,
|
||||
this.floatingActionButton,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final AppBar? appBar;
|
||||
final Widget? body;
|
||||
final FloatingActionButton? floatingActionButton;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Column(
|
||||
children: [
|
||||
const StatusBar(),
|
||||
Expanded(
|
||||
child: Scaffold(
|
||||
appBar: appBar,
|
||||
body: body,
|
||||
floatingActionButton: floatingActionButton,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
113
lib/common/widgets/statusbar.dart
Normal file
@ -0,0 +1,113 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:nm/nm.dart';
|
||||
|
||||
import '../../features/settings/business/wifi_controller.dart';
|
||||
import '../../features/settings/presentation/wifi_settings_screen/widgets/wifi_bar_icon.dart';
|
||||
import 'clock.dart';
|
||||
|
||||
const horizontalSpace = SizedBox(
|
||||
width: 8,
|
||||
);
|
||||
|
||||
class StatusBar extends ConsumerWidget {
|
||||
const StatusBar({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final wifiStream = (Platform.isLinux)
|
||||
? ref.watch(wifiControllerProvider.notifier).wifiStream()
|
||||
: const Stream.empty();
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: 24,
|
||||
color: Colors.black,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Row(
|
||||
children: [
|
||||
horizontalSpace,
|
||||
kDebugMode
|
||||
? Text(
|
||||
'DEBUG MODE',
|
||||
style: TextStyle(color: Colors.white),
|
||||
)
|
||||
: SizedBox(),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
StreamBuilder(
|
||||
stream: wifiStream,
|
||||
builder: (context, snapshot) {
|
||||
if (!Platform.isLinux) {
|
||||
return const Row(
|
||||
children: [
|
||||
Text(
|
||||
'DemoConnection',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
horizontalSpace,
|
||||
WifiBarIcon(
|
||||
strength: 1,
|
||||
size: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
if (snapshot.hasData) {
|
||||
final data = snapshot.data?.$1 as NetworkManagerDevice;
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
data.activeConnection?.id ?? 'No connection',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
horizontalSpace,
|
||||
WifiBarIcon(
|
||||
strength:
|
||||
data.wireless?.activeAccessPoint?.strength ?? 0,
|
||||
size: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
if (snapshot.hasError) {
|
||||
// error can be caught here
|
||||
// display error in statusbar:
|
||||
// return Text(
|
||||
// snapshot.error.toString(),
|
||||
// style: const TextStyle(color: Colors.white),
|
||||
// );
|
||||
// show nothing (hide error message from user):
|
||||
// the error that can happen here is mostly related
|
||||
// to not finding any usable wifi device
|
||||
return const SizedBox();
|
||||
}
|
||||
return const Text(
|
||||
"No connection",
|
||||
style: TextStyle(color: Colors.white),
|
||||
);
|
||||
},
|
||||
),
|
||||
horizontalSpace,
|
||||
const ClockWidget(
|
||||
textStyle: TextStyle(color: Colors.white),
|
||||
),
|
||||
horizontalSpace,
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
97
lib/common/widgets/text_input_dialog.dart
Normal file
@ -0,0 +1,97 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:virtual_keyboard_multi_language/virtual_keyboard_multi_language.dart';
|
||||
|
||||
import '../settings/custom_layout.dart';
|
||||
|
||||
class TextInputDialog extends ConsumerWidget {
|
||||
const TextInputDialog({
|
||||
required this.controller,
|
||||
this.title = 'Text Input',
|
||||
this.onReturnPress,
|
||||
this.hintText,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final TextEditingController controller;
|
||||
final String title;
|
||||
final Function? onReturnPress;
|
||||
final String? hintText;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return TextField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(hintText: hintText),
|
||||
onTap: () {
|
||||
// always move the cursor to the end position by default
|
||||
controller.value = TextEditingValue(
|
||||
text: controller.text,
|
||||
selection: TextSelection.collapsed(
|
||||
offset: controller.text.length,
|
||||
),
|
||||
);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => Dialog.fullscreen(
|
||||
// backgroundColor: Colors.transparent,
|
||||
child: ClipRect(
|
||||
// TODO: Blur background causes lag on the raspberry pi
|
||||
// BackdropFilter() with:
|
||||
// filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
|
||||
child: Scaffold(
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.background.withOpacity(0.5),
|
||||
appBar: AppBar(
|
||||
// backgroundColor: Colors.transparent,
|
||||
title: Text(title),
|
||||
centerTitle: false,
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
FocusScope.of(context).unfocus();
|
||||
context.pop();
|
||||
},
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: TextFieldTapRegion(
|
||||
child: VirtualKeyboard(
|
||||
textController: controller,
|
||||
textColor:
|
||||
Theme.of(context).textTheme.headlineLarge!.color!,
|
||||
type: VirtualKeyboardType.Alphanumeric,
|
||||
// onKeyPress: (key) => onKeyPress(key, context),
|
||||
customLayoutKeys: CustomLayoutKeys(),
|
||||
onReturnKey: onReturnPress,
|
||||
unfocus: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
8
lib/constants/api_urls.dart
Normal file
@ -0,0 +1,8 @@
|
||||
class TagesschauApiUrls {
|
||||
static const main = 'https://www.tagesschau.de/api2/';
|
||||
static const news = '$main/news/';
|
||||
static const placeholderImage =
|
||||
'https://upload.wikimedia.org/wikipedia/commons/thumb/3/3f/Placeholder_view_vector.svg/1362px-Placeholder_view_vector.svg.png';
|
||||
static const logoImage =
|
||||
'https://images.tagesschau.de/image/89045d82-5cd5-46ad-8f91-73911add30ee/AAABh3YLLz0/AAABjcWen7M/16x9-1280/tagesschau-logo-100.jpg';
|
||||
}
|
17
lib/constants/colors.dart
Normal file
@ -0,0 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum ColorSeed {
|
||||
baseColor('M3 Baseline', Color(0xff6750a4)),
|
||||
indigo('Indigo', Colors.indigo),
|
||||
blue('Blue', Colors.blue),
|
||||
teal('Teal', Colors.teal),
|
||||
green('Green', Colors.green),
|
||||
yellow('Yellow', Colors.yellow),
|
||||
orange('Orange', Colors.orange),
|
||||
deepOrange('Deep Orange', Colors.deepOrange),
|
||||
pink('Pink', Colors.pink);
|
||||
|
||||
const ColorSeed(this.label, this.color);
|
||||
final String label;
|
||||
final Color color;
|
||||
}
|
3
lib/features/benchmark/business/flame/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Flame Business Layer
|
||||
|
||||
Custom Layer for the benchmarks that use the Flame engine.
|
21
lib/features/benchmark/business/flame/rogue_shooter/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Blue Fire
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -0,0 +1,10 @@
|
||||
# Flame Performance Test Game
|
||||
|
||||
This is a simple scrolling shooter game which we use for testing the performance of Flame,
|
||||
since it uses a lot of components and hitboxes. When it reaches a certain amount of
|
||||
components (counted in the lower right corner) you can expect it to drop a bit in FPS,
|
||||
depending on what platform you are on.
|
||||
|
||||
# Source
|
||||
|
||||
https://github.com/flame-engine/flame/tree/main/examples/games/rogue_shooter
|
@ -0,0 +1,55 @@
|
||||
import 'package:flame/collisions.dart';
|
||||
import 'package:flame/components.dart';
|
||||
|
||||
import 'enemy_component.dart';
|
||||
|
||||
class BulletComponent extends SpriteAnimationComponent
|
||||
with HasGameRef, CollisionCallbacks {
|
||||
static const speed = 500.0;
|
||||
late final Vector2 velocity;
|
||||
final Vector2 deltaPosition = Vector2.zero();
|
||||
|
||||
BulletComponent({required super.position, super.angle})
|
||||
: super(size: Vector2(10, 20), anchor: Anchor.center);
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
add(CircleHitbox());
|
||||
animation = await game.loadSpriteAnimation(
|
||||
'flame_benchmark/rogue_shooter/bullet.png',
|
||||
SpriteAnimationData.sequenced(
|
||||
stepTime: 0.2,
|
||||
amount: 4,
|
||||
textureSize: Vector2(8, 16),
|
||||
),
|
||||
);
|
||||
velocity = Vector2(0, -1)
|
||||
..rotate(angle)
|
||||
..scale(speed);
|
||||
}
|
||||
|
||||
@override
|
||||
void onCollisionStart(
|
||||
Set<Vector2> intersectionPoints,
|
||||
PositionComponent other,
|
||||
) {
|
||||
super.onCollisionStart(intersectionPoints, other);
|
||||
if (other is EnemyComponent) {
|
||||
other.takeHit();
|
||||
removeFromParent();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void update(double dt) {
|
||||
super.update(dt);
|
||||
deltaPosition
|
||||
..setFrom(velocity)
|
||||
..scale(dt);
|
||||
position += deltaPosition;
|
||||
|
||||
if (position.y < 0 || position.x > game.size.x || position.x + size.x < 0) {
|
||||
removeFromParent();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
import 'package:flame/collisions.dart';
|
||||
import 'package:flame/components.dart';
|
||||
|
||||
import '../rogue_shooter_game.dart';
|
||||
import 'explosion_component.dart';
|
||||
|
||||
class EnemyComponent extends SpriteAnimationComponent
|
||||
with HasGameReference<RogueShooterGame>, CollisionCallbacks {
|
||||
static const speed = 150;
|
||||
static final Vector2 initialSize = Vector2.all(25);
|
||||
|
||||
EnemyComponent({required super.position})
|
||||
: super(size: initialSize, anchor: Anchor.center);
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
animation = await game.loadSpriteAnimation(
|
||||
'flame_benchmark/rogue_shooter/enemy.png',
|
||||
SpriteAnimationData.sequenced(
|
||||
stepTime: 0.2,
|
||||
amount: 4,
|
||||
textureSize: Vector2.all(16),
|
||||
),
|
||||
);
|
||||
add(CircleHitbox(collisionType: CollisionType.passive));
|
||||
}
|
||||
|
||||
@override
|
||||
void update(double dt) {
|
||||
super.update(dt);
|
||||
y += speed * dt;
|
||||
if (y >= game.size.y) {
|
||||
removeFromParent();
|
||||
}
|
||||
}
|
||||
|
||||
void takeHit() {
|
||||
removeFromParent();
|
||||
|
||||
game.add(ExplosionComponent(position: position));
|
||||
game.increaseScore();
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
|
||||
import 'enemy_component.dart';
|
||||
|
||||
class EnemyCreator extends TimerComponent with HasGameRef {
|
||||
final Random random = Random();
|
||||
final _halfWidth = EnemyComponent.initialSize.x / 2;
|
||||
|
||||
EnemyCreator() : super(period: 0.05, repeat: true);
|
||||
|
||||
@override
|
||||
void onTick() {
|
||||
game.addAll(
|
||||
List.generate(
|
||||
5,
|
||||
(index) => EnemyComponent(
|
||||
position: Vector2(
|
||||
_halfWidth + (game.size.x - _halfWidth) * random.nextDouble(),
|
||||
0,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import 'package:flame/components.dart';
|
||||
|
||||
class ExplosionComponent extends SpriteAnimationComponent with HasGameRef {
|
||||
ExplosionComponent({super.position})
|
||||
: super(
|
||||
size: Vector2.all(50),
|
||||
anchor: Anchor.center,
|
||||
removeOnFinish: true,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
animation = await game.loadSpriteAnimation(
|
||||
'flame_benchmark/rogue_shooter/explosion.png',
|
||||
SpriteAnimationData.sequenced(
|
||||
stepTime: 0.1,
|
||||
amount: 6,
|
||||
textureSize: Vector2.all(32),
|
||||
loop: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
import 'package:flame/collisions.dart';
|
||||
import 'package:flame/components.dart';
|
||||
|
||||
import 'bullet_component.dart';
|
||||
import 'enemy_component.dart';
|
||||
import 'explosion_component.dart';
|
||||
|
||||
class PlayerComponent extends SpriteAnimationComponent
|
||||
with HasGameRef, CollisionCallbacks {
|
||||
late TimerComponent bulletCreator;
|
||||
|
||||
PlayerComponent() : super(size: Vector2(50, 75), anchor: Anchor.center);
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
position = game.size / 2;
|
||||
add(CircleHitbox());
|
||||
add(
|
||||
bulletCreator = TimerComponent(
|
||||
period: 0.05,
|
||||
repeat: true,
|
||||
autoStart: false,
|
||||
onTick: _createBullet,
|
||||
),
|
||||
);
|
||||
animation = await game.loadSpriteAnimation(
|
||||
'flame_benchmark/rogue_shooter/player.png',
|
||||
SpriteAnimationData.sequenced(
|
||||
stepTime: 0.2,
|
||||
amount: 4,
|
||||
textureSize: Vector2(32, 39),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final _bulletAngles = [0.5, 0.3, 0.0, -0.5, -0.3];
|
||||
void _createBullet() {
|
||||
game.addAll(
|
||||
_bulletAngles.map(
|
||||
(angle) => BulletComponent(
|
||||
position: position + Vector2(0, -size.y / 2),
|
||||
angle: angle,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void beginFire() {
|
||||
bulletCreator.timer.start();
|
||||
}
|
||||
|
||||
void stopFire() {
|
||||
bulletCreator.timer.pause();
|
||||
}
|
||||
|
||||
void takeHit() {
|
||||
game.add(ExplosionComponent(position: position));
|
||||
}
|
||||
|
||||
@override
|
||||
void onCollisionStart(
|
||||
Set<Vector2> intersectionPoints,
|
||||
PositionComponent other,
|
||||
) {
|
||||
super.onCollisionStart(intersectionPoints, other);
|
||||
if (other is EnemyComponent) {
|
||||
other.takeHit();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/sprite.dart';
|
||||
|
||||
import 'star_component.dart';
|
||||
|
||||
class StarBackGroundCreator extends Component with HasGameRef {
|
||||
final gapSize = 12;
|
||||
|
||||
late final SpriteSheet spriteSheet;
|
||||
Random random = Random();
|
||||
|
||||
StarBackGroundCreator();
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
spriteSheet = SpriteSheet.fromColumnsAndRows(
|
||||
image: await game.images.load('flame_benchmark/rogue_shooter/stars.png'),
|
||||
rows: 4,
|
||||
columns: 4,
|
||||
);
|
||||
|
||||
final starGapTime = (game.size.y / gapSize) / StarComponent.speed;
|
||||
|
||||
add(
|
||||
TimerComponent(
|
||||
period: starGapTime,
|
||||
repeat: true,
|
||||
onTick: () => _createRowOfStars(0),
|
||||
),
|
||||
);
|
||||
|
||||
_createInitialStars();
|
||||
}
|
||||
|
||||
void _createStarAt(double x, double y) {
|
||||
final animation = spriteSheet.createAnimation(
|
||||
row: random.nextInt(3),
|
||||
to: 4,
|
||||
stepTime: 0.1,
|
||||
)..variableStepTimes = [max(20, 100 * random.nextDouble()), 0.1, 0.1, 0.1];
|
||||
|
||||
game.add(StarComponent(animation: animation, position: Vector2(x, y)));
|
||||
}
|
||||
|
||||
void _createRowOfStars(double y) {
|
||||
const gapSize = 6;
|
||||
final starGap = game.size.x / gapSize;
|
||||
|
||||
for (var i = 0; i < gapSize; i++) {
|
||||
_createStarAt(
|
||||
starGap * i + (random.nextDouble() * starGap),
|
||||
y + (random.nextDouble() * 20),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _createInitialStars() {
|
||||
final rows = game.size.y / gapSize;
|
||||
|
||||
for (var i = 0; i < gapSize; i++) {
|
||||
_createRowOfStars(i * rows);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
import 'package:flame/components.dart';
|
||||
|
||||
class StarComponent extends SpriteAnimationComponent with HasGameRef {
|
||||
static const speed = 10;
|
||||
|
||||
StarComponent({super.animation, super.position})
|
||||
: super(size: Vector2.all(20));
|
||||
|
||||
@override
|
||||
void update(double dt) {
|
||||
super.update(dt);
|
||||
y += dt * speed;
|
||||
if (y >= game.size.y) {
|
||||
removeFromParent();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
// import 'package:flame/game.dart';
|
||||
// import 'package:flutter/widgets.dart';
|
||||
// import 'package:rogue_shooter/rogue_shooter_game.dart';
|
||||
|
||||
// void main() {
|
||||
// runApp(GameWidget(game: RogueShooterGame()));
|
||||
// }
|
@ -0,0 +1,36 @@
|
||||
import 'dart:ui';
|
||||
|
||||
// Source: https://github.com/flame-engine/flame/blob/main/packages/flame/lib/src/game/mixins/has_performance_tracker.dart#L6
|
||||
|
||||
import 'package:flame/game.dart';
|
||||
|
||||
/// A mixin that adds performance tracking to a game.
|
||||
mixin HasPerformanceTracker on Game {
|
||||
int _updateTime = 0;
|
||||
int _renderTime = 0;
|
||||
final _stopwatch = Stopwatch();
|
||||
|
||||
/// The time it took to update the game in milliseconds.
|
||||
int get updateTime => _updateTime;
|
||||
|
||||
/// The time it took to render the game in milliseconds.
|
||||
int get renderTime => _renderTime;
|
||||
|
||||
@override
|
||||
void update(double dt) {
|
||||
_stopwatch.reset();
|
||||
_stopwatch.start();
|
||||
super.update(dt);
|
||||
_stopwatch.stop();
|
||||
_updateTime = _stopwatch.elapsedMilliseconds;
|
||||
}
|
||||
|
||||
@override
|
||||
void render(Canvas canvas) {
|
||||
_stopwatch.reset();
|
||||
_stopwatch.start();
|
||||
super.render(canvas);
|
||||
_stopwatch.stop();
|
||||
_renderTime = _stopwatch.elapsedMilliseconds;
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/events.dart';
|
||||
import 'package:flame/game.dart';
|
||||
|
||||
import 'components/enemy_creator.dart';
|
||||
import 'components/player_component.dart';
|
||||
import 'components/star_background_creator.dart';
|
||||
import 'mixins/has_performance_tracker.dart';
|
||||
|
||||
class RogueShooterGame extends FlameGame
|
||||
with PanDetector, HasCollisionDetection, HasPerformanceTracker {
|
||||
static const String description = '''
|
||||
A simple space shooter game used for testing performance of the collision
|
||||
detection system in Flame.
|
||||
''';
|
||||
|
||||
late final PlayerComponent _player;
|
||||
late final TextComponent _componentCounter;
|
||||
late final TextComponent _scoreText;
|
||||
|
||||
final _updateTime = TextComponent(
|
||||
text: 'Update time: 0ms',
|
||||
position: Vector2(90, 20),
|
||||
priority: 1,
|
||||
);
|
||||
|
||||
final TextComponent _renderTime = TextComponent(
|
||||
text: 'Render time: 0ms',
|
||||
position: Vector2(90, 45),
|
||||
priority: 1,
|
||||
);
|
||||
|
||||
int _score = 0;
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
add(_player = PlayerComponent());
|
||||
addAll([
|
||||
FpsTextComponent(
|
||||
position: size - Vector2(0, 50),
|
||||
anchor: Anchor.bottomRight,
|
||||
),
|
||||
_scoreText = TextComponent(
|
||||
position: size - Vector2(0, 25),
|
||||
anchor: Anchor.bottomRight,
|
||||
priority: 1,
|
||||
),
|
||||
_componentCounter = TextComponent(
|
||||
position: size,
|
||||
anchor: Anchor.bottomRight,
|
||||
priority: 1,
|
||||
),
|
||||
]);
|
||||
|
||||
add(EnemyCreator());
|
||||
add(StarBackGroundCreator());
|
||||
|
||||
addAll([_updateTime, _renderTime]);
|
||||
}
|
||||
|
||||
@override
|
||||
void update(double dt) {
|
||||
super.update(dt);
|
||||
_scoreText.text = 'Score: $_score';
|
||||
_componentCounter.text = 'Components: ${children.length}';
|
||||
_updateTime.text = 'Update time: $updateTime ms';
|
||||
_renderTime.text = 'Render time: $renderTime ms';
|
||||
}
|
||||
|
||||
@override
|
||||
void onPanStart(_) {
|
||||
_player.beginFire();
|
||||
}
|
||||
|
||||
@override
|
||||
void onPanEnd(_) {
|
||||
_player.stopFire();
|
||||
}
|
||||
|
||||
@override
|
||||
void onPanCancel() {
|
||||
_player.stopFire();
|
||||
}
|
||||
|
||||
@override
|
||||
void onPanUpdate(DragUpdateInfo info) {
|
||||
_player.position += info.delta.global;
|
||||
}
|
||||
|
||||
void increaseScore() {
|
||||
_score++;
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'rogue_shooter_game.dart';
|
||||
|
||||
class RogueShooterWidget extends StatelessWidget {
|
||||
const RogueShooterWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GameWidget(
|
||||
game: RogueShooterGame(),
|
||||
loadingBuilder: (_) => const Center(
|
||||
child: Text('Loading'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Blue Fire
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -0,0 +1,12 @@
|
||||
# Flame Engine Sprite Renderer Benchmark
|
||||
|
||||
This is a simple benchmark to test the render
|
||||
performance of the Flutter Render Engine.
|
||||
|
||||
It was modified from: https://github.com/flame-engine/flame/blob/main/examples/lib/stories/animations/benchmark_example.dart
|
||||
|
||||
## Modifications
|
||||
|
||||
- added framerate display
|
||||
- add sprites automatically for 30 minutes and only if the framerate is above 59
|
||||
- timer and benchmark test starts automatically and displays the result after 30 minutes
|
@ -0,0 +1,14 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/events.dart';
|
||||
|
||||
import 'sprite_benchmark.dart';
|
||||
|
||||
class BenchmarkWorld extends World
|
||||
with TapCallbacks, HasGameReference<SpriteBenchmark> {
|
||||
final Random random = Random();
|
||||
|
||||
@override
|
||||
void onTapDown(TapDownEvent event) {}
|
||||
}
|
@ -0,0 +1,144 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../data/models/ember.dart';
|
||||
import 'benchmark_world.dart';
|
||||
|
||||
class SpriteBenchmark extends FlameGame with HasGameRef {
|
||||
static const description = '''
|
||||
See how many SpriteAnimationComponent's your platform can handle before it
|
||||
starts to drop in FPS, this is without any sprite batching and such.
|
||||
''';
|
||||
|
||||
SpriteBenchmark() : super(world: BenchmarkWorld());
|
||||
|
||||
// 30 minutes countdown
|
||||
final countdown = Timer(1800);
|
||||
|
||||
final emberSize = Vector2.all(20);
|
||||
late final TextComponent emberCounter;
|
||||
final counterPrefix = 'Animations: ';
|
||||
double posX = 0;
|
||||
double posY = 0;
|
||||
|
||||
final Queue<double> window = Queue();
|
||||
|
||||
@override
|
||||
Color backgroundColor() => Colors.black;
|
||||
|
||||
double _sum = 0;
|
||||
int spriteCount = 0;
|
||||
|
||||
final int windowSize = 60;
|
||||
|
||||
TextPaint textPaint = TextPaint(
|
||||
style: TextStyle(
|
||||
fontSize: 20.0,
|
||||
fontFamily: 'Awesome Font',
|
||||
background: Paint()
|
||||
..color = Colors.indigo
|
||||
..strokeJoin = StrokeJoin.round
|
||||
..strokeCap = StrokeCap.round
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 25.0,
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await camera.viewport.addAll([
|
||||
FpsTextComponent(
|
||||
position: size - Vector2(20, 80),
|
||||
anchor: Anchor.bottomRight,
|
||||
textRenderer: textPaint,
|
||||
),
|
||||
emberCounter = TextComponent(
|
||||
position: size - Vector2(20, 25),
|
||||
anchor: Anchor.bottomRight,
|
||||
priority: 1,
|
||||
textRenderer: textPaint,
|
||||
),
|
||||
]);
|
||||
// world.add(Ember(size: emberSize));
|
||||
children.register<Ember>();
|
||||
}
|
||||
|
||||
double get fps {
|
||||
return window.isEmpty ? 0 : window.length / _sum;
|
||||
}
|
||||
|
||||
@override
|
||||
void update(double dt) async {
|
||||
// add text to the screen only once
|
||||
if (countdown.finished) {
|
||||
showDialog(
|
||||
context: gameRef.buildContext!,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Benchmark finished'),
|
||||
content: Text(
|
||||
'After 30 minutes, the benchmark finished with $spriteCount sprite animations.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('Close'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
countdown.reset();
|
||||
}
|
||||
if (!countdown.finished && countdown.isRunning()) {
|
||||
countdown.update(dt);
|
||||
|
||||
window.addLast(dt);
|
||||
_sum += dt;
|
||||
if (window.length > windowSize) {
|
||||
_sum -= window.removeFirst();
|
||||
}
|
||||
if (fps > 59) {
|
||||
if (posY > size.y) {
|
||||
final Random random = Random();
|
||||
add(
|
||||
Ember(
|
||||
size: emberSize,
|
||||
position: Vector2(
|
||||
(size.x) * random.nextDouble() * (random.nextBool() ? 1 : -1),
|
||||
(size.y) * random.nextDouble() * (random.nextBool() ? 1 : -1),
|
||||
),
|
||||
),
|
||||
);
|
||||
sleep(const Duration(milliseconds: 50));
|
||||
} else {
|
||||
for (int i = 0; i < 1; i++) {
|
||||
add(Ember(
|
||||
size: emberSize,
|
||||
position: Vector2(10 + posX, 10 + posY),
|
||||
));
|
||||
if (posX < size.x) {
|
||||
posX += 20;
|
||||
} else {
|
||||
posY += 20;
|
||||
posX = 0;
|
||||
}
|
||||
}
|
||||
sleep(const Duration(milliseconds: 50));
|
||||
}
|
||||
spriteCount += 1;
|
||||
}
|
||||
super.update(dt);
|
||||
emberCounter.text = '$counterPrefix $spriteCount';
|
||||
}
|
||||
}
|
||||
}
|
31
lib/features/benchmark/data/benchmarks_list.dart
Normal file
@ -0,0 +1,31 @@
|
||||
import '../../../common/routing/routes.dart';
|
||||
import 'models/benchmark.dart';
|
||||
|
||||
final List<Benchmark> benchmarksList = [
|
||||
Benchmark(
|
||||
title: 'Flame Sprite Rendering',
|
||||
desc: 'Tests how many sprites can be renderer without a frame drop.',
|
||||
route: RoutesBenchmark.sprites,
|
||||
),
|
||||
Benchmark(
|
||||
title: 'Rogue Shooter Benchmark',
|
||||
desc:
|
||||
'A simple scrolling shooter game which is used by the Flame Engines team for testing the performance of Flame.',
|
||||
route: RoutesBenchmark.rogueShooter,
|
||||
),
|
||||
Benchmark(
|
||||
title: 'VSync Test',
|
||||
desc: 'Tests if the VSync is working properly with a fragment shader.',
|
||||
route: RoutesBenchmark.shader,
|
||||
),
|
||||
Benchmark(
|
||||
title: 'Video Benchmark',
|
||||
desc: 'Video Player demo.',
|
||||
route: RoutesBenchmark.video,
|
||||
),
|
||||
Benchmark(
|
||||
title: 'Simple Tiles Map Benchmark',
|
||||
desc: 'Map demo.',
|
||||
route: RoutesBenchmark.map,
|
||||
),
|
||||
];
|
15
lib/features/benchmark/data/models/benchmark.dart
Normal file
@ -0,0 +1,15 @@
|
||||
import '../../../../common/routing/routes.dart';
|
||||
|
||||
class Benchmark {
|
||||
final String title;
|
||||
final String desc;
|
||||
final String buttonText;
|
||||
final RoutesBenchmark route;
|
||||
|
||||
Benchmark({
|
||||
required this.title,
|
||||
required this.desc,
|
||||
this.buttonText = 'Go to benchmark',
|
||||
required this.route,
|
||||
});
|
||||
}
|
25
lib/features/benchmark/data/models/ember.dart
Normal file
@ -0,0 +1,25 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
class Ember<T extends FlameGame> extends SpriteAnimationComponent
|
||||
with HasGameReference<T> {
|
||||
Ember({super.position, Vector2? size, super.priority, super.key})
|
||||
: super(
|
||||
size: size ?? Vector2.all(50),
|
||||
anchor: Anchor.center,
|
||||
);
|
||||
|
||||
@mustCallSuper
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
animation = await game.loadSpriteAnimation(
|
||||
'flame_benchmark/sprite_benchmark/ember.png',
|
||||
SpriteAnimationData.sequenced(
|
||||
amount: 3,
|
||||
textureSize: Vector2.all(16),
|
||||
stepTime: 0.15,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
27
lib/features/benchmark/presentation/benchmark_screen.dart
Normal file
@ -0,0 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import '../data/benchmarks_list.dart';
|
||||
import 'widgets/benchmark_card.dart';
|
||||
|
||||
class BenchmarkScreen extends StatelessWidget {
|
||||
const BenchmarkScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GridView.builder(
|
||||
padding: const EdgeInsets.all(12),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: MediaQuery.of(context).size.shortestSide < 600 ? 2 : 3,
|
||||
crossAxisSpacing: 12.0,
|
||||
mainAxisSpacing: 12,
|
||||
),
|
||||
itemCount: benchmarksList.length,
|
||||
itemBuilder: (context, index) {
|
||||
return BenchmarkCard(
|
||||
benchmark: benchmarksList[index],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_elinux/common/widgets/floating_menu_button.dart';
|
||||
import 'package:flutter_elinux/common/widgets/scaffold_with_statusbar.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:simple_tiles_map/simple_tiles_map.dart';
|
||||
|
||||
class MapBenchmarkScreen extends StatefulWidget {
|
||||
const MapBenchmarkScreen({super.key});
|
||||
|
||||
@override
|
||||
State<MapBenchmarkScreen> createState() => _MapBenchmarkScreenState();
|
||||
}
|
||||
|
||||
class _MapBenchmarkScreenState extends State<MapBenchmarkScreen> {
|
||||
TypeMap _typeMap = TypeMap.osm;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
MapOptions mapOptions = MapOptions(
|
||||
maxZoom: 19,
|
||||
minZoom: 5,
|
||||
);
|
||||
return ScaffoldWithStatusbar(
|
||||
body: Stack(children: [
|
||||
Column(
|
||||
children: [
|
||||
SimpleTilesMap(
|
||||
typeMap: _typeMap,
|
||||
mapOptions: mapOptions,
|
||||
),
|
||||
],
|
||||
),
|
||||
FloatingMenuButton(
|
||||
popupMenuButton: PopupMenuButton(
|
||||
itemBuilder: (BuildContext context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
value: TypeMap.google,
|
||||
enabled: (_typeMap != TypeMap.google),
|
||||
child: const Text('Google'),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: TypeMap.googleHybrid,
|
||||
enabled: (_typeMap != TypeMap.googleHybrid),
|
||||
child: const Text('Google Hybrid'),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: TypeMap.googleSatellite,
|
||||
enabled: (_typeMap != TypeMap.googleSatellite),
|
||||
child: const Text('Google Satellite'),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: TypeMap.osm,
|
||||
enabled: (_typeMap != TypeMap.osm),
|
||||
child: const Text('Open Street Map'),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: TypeMap.cartoMapPositron,
|
||||
enabled: (_typeMap != TypeMap.cartoMapPositron),
|
||||
child: const Text('Carto Map Positron'),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: TypeMap.cartoMapDark,
|
||||
enabled: (_typeMap != TypeMap.cartoMapDark),
|
||||
child: const Text('Carto Map Dark'),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: TypeMap.esriSatellite,
|
||||
enabled: (_typeMap != TypeMap.esriSatellite),
|
||||
child: const Text('Esri Satellite'),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: TypeMap.esriStreets,
|
||||
enabled: (_typeMap != TypeMap.esriStreets),
|
||||
child: const Text('Esri Streets'),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: TypeMap.esriTopo,
|
||||
enabled: (_typeMap != TypeMap.esriTopo),
|
||||
child: const Text('Esri Topo'),
|
||||
),
|
||||
];
|
||||
},
|
||||
onSelected: (dynamic value) {
|
||||
setState(() {
|
||||
_typeMap = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../common/widgets/floating_menu_button.dart';
|
||||
import '../business/flame/rogue_shooter/rogue_shooter_game.dart';
|
||||
|
||||
class RogueShooterScreen extends StatefulWidget {
|
||||
const RogueShooterScreen({super.key});
|
||||
|
||||
@override
|
||||
State<RogueShooterScreen> createState() => _RogueShooterScreenState();
|
||||
}
|
||||
|
||||
class _RogueShooterScreenState extends State<RogueShooterScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Stack(children: [
|
||||
GameWidget(game: RogueShooterGame()),
|
||||
const FloatingMenuButton(),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../common/widgets/floating_menu_button.dart';
|
||||
import '../business/flame/sprite_renderer/sprite_benchmark.dart';
|
||||
|
||||
class SpriteBenchmarkScreen extends StatelessWidget {
|
||||
const SpriteBenchmarkScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Stack(children: [
|
||||
GameWidget(
|
||||
game: SpriteBenchmark(),
|
||||
),
|
||||
const FloatingMenuButton(),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
111
lib/features/benchmark/presentation/video_benchmark_screen.dart
Normal file
@ -0,0 +1,111 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
class VideoBenchmarkScreen extends StatefulWidget {
|
||||
const VideoBenchmarkScreen({super.key});
|
||||
|
||||
@override
|
||||
State<VideoBenchmarkScreen> createState() => _VideoBenchmarkScreenState();
|
||||
}
|
||||
|
||||
class _VideoBenchmarkScreenState extends State<VideoBenchmarkScreen> {
|
||||
late VideoPlayerController _controller;
|
||||
String selectedMode = '720p';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = VideoPlayerController.asset(
|
||||
'assets/video/video_720p.mp4',
|
||||
)
|
||||
..initialize().then((_) {
|
||||
setState(() {});
|
||||
})
|
||||
..setLooping(true);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_controller.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Video Player Demo ($selectedMode)'),
|
||||
actions: [
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
_controller = VideoPlayerController.asset(
|
||||
'assets/video/video_720p.mp4',
|
||||
)
|
||||
..initialize().then((_) {
|
||||
setState(() {
|
||||
selectedMode = '720p';
|
||||
});
|
||||
})
|
||||
..setLooping(true);
|
||||
},
|
||||
child: const Text('load 720p')),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
_controller = VideoPlayerController.asset(
|
||||
'assets/video/video_1080p.mp4',
|
||||
)
|
||||
..initialize().then((_) {
|
||||
setState(() {
|
||||
selectedMode = '1080p';
|
||||
});
|
||||
})
|
||||
..setLooping(true);
|
||||
},
|
||||
child: const Text('load 1080p')),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
_controller = VideoPlayerController.asset(
|
||||
'assets/video/video_2160p.mp4',
|
||||
)
|
||||
..initialize().then((_) {
|
||||
setState(() {
|
||||
selectedMode = '2160p';
|
||||
});
|
||||
})
|
||||
..setLooping(true);
|
||||
},
|
||||
child: const Text('load 2160p')),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Center(
|
||||
child: _controller.value.isInitialized
|
||||
? AspectRatio(
|
||||
aspectRatio: _controller.value.aspectRatio,
|
||||
child: VideoPlayer(_controller),
|
||||
)
|
||||
: Container(),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_controller.value.isPlaying
|
||||
? _controller.pause()
|
||||
: _controller.play();
|
||||
});
|
||||
},
|
||||
child: Icon(
|
||||
_controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
import '../../../common/widgets/floating_menu_button.dart';
|
||||
|
||||
class VsyncBenchmarkScreen extends StatelessWidget {
|
||||
const VsyncBenchmarkScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: ShaderHomePage(),
|
||||
),
|
||||
FloatingMenuButton(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ShaderHomePage extends StatefulWidget {
|
||||
const ShaderHomePage({super.key});
|
||||
|
||||
@override
|
||||
State<ShaderHomePage> createState() => _ShaderHomePageState();
|
||||
}
|
||||
|
||||
class _ShaderHomePageState extends State<ShaderHomePage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
double delta = 0;
|
||||
FragmentShader? shader;
|
||||
|
||||
late final Ticker _ticker;
|
||||
|
||||
void loadMyShader() async {
|
||||
var program = await FragmentProgram.fromAsset('shaders/vsync.frag');
|
||||
shader = program.fragmentShader();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
loadMyShader();
|
||||
_ticker = createTicker((elapsed) {
|
||||
setState(() {
|
||||
delta += 1 / 60;
|
||||
});
|
||||
});
|
||||
_ticker.start();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ticker.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (shader == null) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else {
|
||||
return Container(
|
||||
color: Colors.white,
|
||||
child: CustomPaint(painter: ShaderPainter(shader!, delta)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ShaderPainter extends CustomPainter {
|
||||
final FragmentShader shader;
|
||||
final double time;
|
||||
|
||||
ShaderPainter(FragmentShader fragmentShader, this.time)
|
||||
: shader = fragmentShader;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint();
|
||||
shader.setFloat(0, size.width);
|
||||
shader.setFloat(1, size.height);
|
||||
shader.setFloat(2, time);
|
||||
paint.shader = shader;
|
||||
canvas.drawRect(Offset.zero & size, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../data/models/benchmark.dart';
|
||||
|
||||
class BenchmarkCard extends StatelessWidget {
|
||||
const BenchmarkCard({
|
||||
required this.benchmark,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Benchmark benchmark;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Text(
|
||||
benchmark.title,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 18),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
child: Text(
|
||||
benchmark.desc,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.pushNamed(benchmark.route.name);
|
||||
},
|
||||
child: Text(benchmark.buttonText),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
34
lib/features/home/presentation/home_screen.dart
Normal file
@ -0,0 +1,34 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@override
|
||||
State<HomeScreen> createState() => _HomeScreenState();
|
||||
}
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'Flutter on Embedded Linux',
|
||||
style: Theme.of(context).textTheme.headlineLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,365 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'widgets/component_decoration.dart';
|
||||
|
||||
// https://github.com/flutter/samples/blob/main/material_3_demo/lib/component_screen.dart
|
||||
|
||||
const rowDivider = SizedBox(width: 20);
|
||||
const colDivider = SizedBox(height: 10);
|
||||
|
||||
const tinySpacing = 3.0;
|
||||
const smallSpacing = 10.0;
|
||||
const double cardWidth = 115;
|
||||
const double widthConstraint = 450;
|
||||
|
||||
class MaterialDemoScreen extends StatefulWidget {
|
||||
const MaterialDemoScreen({super.key});
|
||||
|
||||
@override
|
||||
State<MaterialDemoScreen> createState() => _MaterialDemoScreenState();
|
||||
}
|
||||
|
||||
class _MaterialDemoScreenState extends State<MaterialDemoScreen> {
|
||||
bool switch1 = false;
|
||||
bool switch2 = true;
|
||||
|
||||
double sliderValue1 = 0.2;
|
||||
double sliderValue2 = 0.5;
|
||||
|
||||
int navBarIndex = 0;
|
||||
|
||||
bool indicatorPlayAnimation = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 84,
|
||||
),
|
||||
ComponentDecoration(
|
||||
title: 'Common Buttons',
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () {},
|
||||
child: const Text('Elevated'),
|
||||
),
|
||||
rowDivider,
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Icon'),
|
||||
),
|
||||
rowDivider,
|
||||
const ElevatedButton(
|
||||
style: ButtonStyle(),
|
||||
onPressed: null,
|
||||
child: Text('Elevated'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
FilledButton(
|
||||
onPressed: () {},
|
||||
child: const Text('Filled'),
|
||||
),
|
||||
rowDivider,
|
||||
FilledButton.icon(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Icon'),
|
||||
),
|
||||
rowDivider,
|
||||
const FilledButton(
|
||||
style: ButtonStyle(),
|
||||
onPressed: null,
|
||||
child: Text('Filled'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
child: const Text('Text'),
|
||||
),
|
||||
rowDivider,
|
||||
TextButton.icon(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Icon'),
|
||||
),
|
||||
rowDivider,
|
||||
const TextButton(
|
||||
style: ButtonStyle(),
|
||||
onPressed: null,
|
||||
child: Text('Text'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ComponentDecoration(
|
||||
title: 'Switches',
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Switch(
|
||||
value: switch1,
|
||||
onChanged: (bool value) {
|
||||
setState(() {
|
||||
switch1 = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
Switch(
|
||||
value: switch2,
|
||||
thumbIcon: MaterialStateProperty.resolveWith<Icon?>(
|
||||
(Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.selected)) {
|
||||
return const Icon(Icons.check);
|
||||
}
|
||||
return null; // All other states will use the default thumbIcon.
|
||||
}),
|
||||
onChanged: (bool value) {
|
||||
setState(() {
|
||||
switch2 = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ComponentDecoration(
|
||||
title: 'Floating Action Buttons',
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
FloatingActionButton.small(
|
||||
onPressed: () {},
|
||||
tooltip: 'Small',
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
FloatingActionButton(
|
||||
onPressed: () {},
|
||||
tooltip: 'Standard',
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
FloatingActionButton.extended(
|
||||
onPressed: () {},
|
||||
tooltip: 'Extended',
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Create'),
|
||||
),
|
||||
FloatingActionButton.large(
|
||||
onPressed: () {},
|
||||
tooltip: 'Large',
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ComponentDecoration(
|
||||
title: 'Sliders',
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Slider(
|
||||
value: sliderValue1,
|
||||
onChanged: (double value) {
|
||||
setState(() {
|
||||
sliderValue1 = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
Slider(
|
||||
value: sliderValue2,
|
||||
onChanged: (double value) {
|
||||
setState(() {
|
||||
sliderValue2 = value;
|
||||
});
|
||||
},
|
||||
divisions: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ComponentDecoration(
|
||||
title: 'Loading Indicators',
|
||||
child: Column(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
indicatorPlayAnimation = !indicatorPlayAnimation;
|
||||
});
|
||||
},
|
||||
icon: indicatorPlayAnimation
|
||||
? const Icon(Icons.pause)
|
||||
: const Icon(Icons.play_arrow),
|
||||
),
|
||||
colDivider,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
rowDivider,
|
||||
CircularProgressIndicator(
|
||||
value: indicatorPlayAnimation ? null : 0.5,
|
||||
),
|
||||
rowDivider,
|
||||
Expanded(
|
||||
child: LinearProgressIndicator(
|
||||
value: indicatorPlayAnimation ? null : 0.5,
|
||||
),
|
||||
),
|
||||
rowDivider,
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ComponentDecoration(
|
||||
title: 'Dialogs and BottomSheets',
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
// https://api.flutter.dev/flutter/material/showModalBottomSheet.html
|
||||
return SizedBox(
|
||||
height: 200,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
const Text('Modal BottomSheet'),
|
||||
ElevatedButton(
|
||||
child: const Text('Close BottomSheet'),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: const Text('show ModalBottomSheet'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Dialog Title'),
|
||||
content: const Text('Dialog description'),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Close'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: const Text('show Dialog'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return Dialog.fullscreen(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Fullscreen Dialog'),
|
||||
),
|
||||
body: Center(
|
||||
child: ElevatedButton(
|
||||
child: const Text('Close Dialog'),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: const Text('show Fullscreen Dialog'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ComponentDecoration(
|
||||
title: 'Navigation Bar',
|
||||
child: NavigationBar(
|
||||
selectedIndex: navBarIndex,
|
||||
onDestinationSelected: (value) {
|
||||
setState(() {
|
||||
navBarIndex = value;
|
||||
});
|
||||
},
|
||||
destinations: const [
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.home),
|
||||
label: 'Home',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.favorite),
|
||||
label: 'Favorites',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.search),
|
||||
label: 'Search',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.settings),
|
||||
label: 'Settings',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ComponentDecoration(
|
||||
title: 'Bottom App Bar',
|
||||
child: BottomAppBar(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.menu),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.search),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.more_vert),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ComponentDecoration extends StatelessWidget {
|
||||
const ComponentDecoration(
|
||||
{required this.child, required this.title, super.key});
|
||||
|
||||
final Widget child;
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(title),
|
||||
const SizedBox(width: 8),
|
||||
const Icon(
|
||||
Icons.info_outline,
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outlineVariant,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 5.0,
|
||||
vertical: 20.0,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
28
lib/features/matrix_rgb/business/service/matrix_service.dart
Normal file
@ -0,0 +1,28 @@
|
||||
import 'package:dart_periphery/dart_periphery.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
// linux_spidev was tried out here beforehand,
|
||||
// but it didn't work and was replaced by dart_periphery
|
||||
// import 'package:linux_spidev/linux_spidev.dart';
|
||||
|
||||
class MatrixService {
|
||||
final spi = SPI(0, 0, SPImode.mode0, 35000);
|
||||
// final spidev = Spidev.fromBusDevNrs(0, 0);
|
||||
|
||||
void setLedsOn(List<int> pixels) async {
|
||||
// final handle = spidev.open();
|
||||
try {
|
||||
int refresh = 1;
|
||||
while (refresh != 0) {
|
||||
spi.transfer(pixels, false);
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
refresh--;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
} finally {
|
||||
// clean up the resource
|
||||
spi.dispose();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
import 'dart:ui';
|
||||
|
||||
int findClosestColorIndex(Color sourceColor) {
|
||||
int bestMatchIndex = 0;
|
||||
double smallestDistance = double.infinity; // Start with a 'large' distance
|
||||
|
||||
// Iterate through possible controller colors (0-255)
|
||||
for (int i = 0; i < 256; i++) {
|
||||
// Example: Assume your controller just uses an even R/G/B scaling
|
||||
int r = (i >> 5) * 32; // Assuming an even split to get 8 values
|
||||
int g = ((i >> 2) & 0x7) * 32;
|
||||
int b = (i & 0x3) * 64;
|
||||
|
||||
// Color controllerColor = Color.fromARGB(255, r, g, b);
|
||||
|
||||
// Calculate distance (you can use different formulas)
|
||||
int distance = (sourceColor.red - r) * (sourceColor.red - r) +
|
||||
(sourceColor.green - g) * (sourceColor.green - g) +
|
||||
(sourceColor.blue - b) * (sourceColor.blue - b);
|
||||
|
||||
if (distance < smallestDistance) {
|
||||
smallestDistance = distance.toDouble();
|
||||
bestMatchIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
return bestMatchIndex;
|
||||
}
|
11
lib/features/matrix_rgb/data/models/pixel.dart
Normal file
@ -0,0 +1,11 @@
|
||||
import 'dart:ui';
|
||||
|
||||
class Pixel {
|
||||
final Offset offset;
|
||||
final Color color;
|
||||
|
||||
Pixel({
|
||||
required this.offset,
|
||||
required this.color,
|
||||
});
|
||||
}
|
143
lib/features/matrix_rgb/presentation/matrix_screen.dart
Normal file
@ -0,0 +1,143 @@
|
||||
import 'package:flex_color_picker/flex_color_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../data/models/pixel.dart';
|
||||
import 'widgets/pixel_painter.dart';
|
||||
|
||||
class MatrixScreen extends StatefulWidget {
|
||||
const MatrixScreen({super.key});
|
||||
|
||||
@override
|
||||
State<MatrixScreen> createState() => _MatrixScreenState();
|
||||
}
|
||||
|
||||
class _MatrixScreenState extends State<MatrixScreen> {
|
||||
List<Pixel> pixels = List.generate(
|
||||
64,
|
||||
(index) => Pixel(
|
||||
offset: Offset.zero,
|
||||
color: Colors.transparent,
|
||||
),
|
||||
);
|
||||
Color color = Colors.green;
|
||||
|
||||
void _updatePixel(Offset offset) {
|
||||
const double cellSize = 50; // Size of each grid cell
|
||||
|
||||
// Get the row and column based on touch position
|
||||
int row = (offset.dy / cellSize).floor();
|
||||
int col = (offset.dx / cellSize).floor();
|
||||
|
||||
// Bound Check (Only proceed if within the grid)
|
||||
if (row >= 0 && row < 8 && col >= 0 && col < 8) {
|
||||
// Calculate the index in the flat list
|
||||
int index = row * 8 + col;
|
||||
|
||||
// Update the Pixel
|
||||
pixels[index] = Pixel(offset: offset, color: color);
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Listener(
|
||||
onPointerDown: (event) {
|
||||
// Determine offset
|
||||
Offset offset =
|
||||
Offset(event.localPosition.dx, event.localPosition.dy);
|
||||
|
||||
// Calculate index and update pixel
|
||||
_updatePixel(offset);
|
||||
},
|
||||
onPointerMove: (event) {
|
||||
// Determine offset
|
||||
Offset offset =
|
||||
Offset(event.localPosition.dx, event.localPosition.dy);
|
||||
|
||||
// Calculate index and update pixel
|
||||
_updatePixel(offset);
|
||||
},
|
||||
child: InteractiveViewer(
|
||||
maxScale: 5,
|
||||
child: Container(
|
||||
// color: Colors.white,
|
||||
decoration: BoxDecoration(border: Border.all()),
|
||||
child: ClipRRect(
|
||||
child: GridPaper(
|
||||
divisions: 2,
|
||||
interval: 500,
|
||||
color: Theme.of(context).textTheme.headlineLarge?.color ??
|
||||
Colors.red,
|
||||
child: Transform(
|
||||
transform: Matrix4.identity()..scale(50.0),
|
||||
child: ClipRect(
|
||||
child: CustomPaint(
|
||||
size: const Size(400, 400),
|
||||
painter: PixelPainter(
|
||||
pixels: pixels,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 24,
|
||||
),
|
||||
ColorIndicator(
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 24,
|
||||
color: color,
|
||||
elevation: 1,
|
||||
onSelectFocus: false,
|
||||
onSelect: () async {
|
||||
// Wait for the dialog to return color selection result.
|
||||
final Color newColor = await showColorPickerDialog(
|
||||
context,
|
||||
color,
|
||||
title: Text('ColorPicker',
|
||||
style: Theme.of(context).textTheme.titleLarge),
|
||||
width: 40,
|
||||
height: 40,
|
||||
spacing: 0,
|
||||
runSpacing: 0,
|
||||
borderRadius: 0,
|
||||
wheelDiameter: 165,
|
||||
enableOpacity: true,
|
||||
showColorCode: true,
|
||||
colorCodeHasColor: true,
|
||||
pickersEnabled: <ColorPickerType, bool>{
|
||||
ColorPickerType.wheel: true,
|
||||
},
|
||||
copyPasteBehavior: const ColorPickerCopyPasteBehavior(
|
||||
copyButton: true,
|
||||
pasteButton: true,
|
||||
longPressMenu: true,
|
||||
),
|
||||
actionButtons: const ColorPickerActionButtons(
|
||||
okButton: true,
|
||||
closeButton: true,
|
||||
dialogActionButtons: false,
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: 480, minWidth: 320, maxWidth: 320),
|
||||
);
|
||||
// Update the state with the newly selected color from the color picker
|
||||
setState(() {
|
||||
color = newColor;
|
||||
});
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../data/models/pixel.dart';
|
||||
import '../../business/service/matrix_service.dart';
|
||||
import '../../business/utils/find_closest_color_index.dart';
|
||||
|
||||
class PixelPainter extends CustomPainter {
|
||||
final List<Pixel> pixels;
|
||||
|
||||
PixelPainter({required this.pixels});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
const double cellSize = 50;
|
||||
|
||||
if (pixels.isNotEmpty) {
|
||||
for (final pixel in pixels) {
|
||||
final Offset roundedOffset = Offset(
|
||||
(pixel.offset.dx / cellSize).clamp(0, 7).toInt() * cellSize + 25,
|
||||
(pixel.offset.dy / cellSize).clamp(0, 7).toInt() * cellSize + 25,
|
||||
);
|
||||
|
||||
canvas.drawPoints(
|
||||
PointMode.points,
|
||||
[roundedOffset / 50],
|
||||
Paint()
|
||||
..color = pixel.color
|
||||
..strokeWidth = 1
|
||||
..isAntiAlias = false
|
||||
..strokeJoin = StrokeJoin.round
|
||||
..style = PaintingStyle.stroke,
|
||||
);
|
||||
}
|
||||
|
||||
// Assuming 'width' represents the width of your matrix (in this case, 8)
|
||||
const width = 8;
|
||||
|
||||
List<int> colorIntList = pixels.map((pixel) {
|
||||
if (pixel.color.value != 0) return findClosestColorIndex(pixel.color);
|
||||
return pixel.color.value;
|
||||
}).toList();
|
||||
|
||||
// horizontal mirroring
|
||||
List<int> mirroredList = List.generate(64, (index) {
|
||||
int row = index ~/ width;
|
||||
int reversedCol = width - 1 - (index % width);
|
||||
int newIndex = row * width + reversedCol;
|
||||
return colorIntList[newIndex];
|
||||
});
|
||||
|
||||
// dart periphery is only supported on linux
|
||||
if (Platform.isLinux) {
|
||||
MatrixService().setLedsOn(mirroredList.reversed.toList());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
||||
}
|
102
lib/features/morse_led/business/service/morse_service.dart
Normal file
@ -0,0 +1,102 @@
|
||||
import 'package:flutter_gpiod/flutter_gpiod.dart';
|
||||
|
||||
class MorseService {
|
||||
final _chips = FlutterGpiod.instance.chips;
|
||||
|
||||
printMessage(String message) async {
|
||||
final chip = _getRaspberryGPIO();
|
||||
final ledPin = chip.lines[18];
|
||||
ledPin.requestOutput(consumer: "morse code led", initialValue: false);
|
||||
final List<String> chars = message.toUpperCase().split('');
|
||||
const int pulseDuration = 100;
|
||||
|
||||
for (var char in chars) {
|
||||
final List<String> morseCode = _morseAlphabet[char]?.split('') ?? ['*'];
|
||||
for (var code in morseCode) {
|
||||
print('CODE: $code from $morseCode');
|
||||
if (code == '*') {
|
||||
await Future.delayed(const Duration(milliseconds: pulseDuration * 7));
|
||||
break;
|
||||
}
|
||||
ledPin.setValue(true);
|
||||
print('ON');
|
||||
if (code == '.') {
|
||||
await Future.delayed(const Duration(milliseconds: pulseDuration));
|
||||
} else if (code == '-') {
|
||||
await Future.delayed(const Duration(milliseconds: pulseDuration * 3));
|
||||
}
|
||||
print('OFF');
|
||||
ledPin.setValue(false);
|
||||
// pause between letters
|
||||
await Future.delayed(const Duration(milliseconds: pulseDuration));
|
||||
}
|
||||
print("SPACE BETWEEN LETTERS");
|
||||
// space between letters is 3*pulseDuration
|
||||
await Future.delayed(const Duration(milliseconds: pulseDuration * 3));
|
||||
}
|
||||
|
||||
ledPin.release();
|
||||
return true;
|
||||
}
|
||||
|
||||
GpioChip _getRaspberryGPIO() {
|
||||
/// Retrieve the line with index 23 of the first chip.
|
||||
/// This is BCM pin 23 for the Raspberry Pi.
|
||||
///
|
||||
/// I recommend finding the chip you want
|
||||
/// based on the chip label, as is done here.
|
||||
///
|
||||
/// In this example, we search for the main Raspberry Pi GPIO chip and then
|
||||
/// retrieve the line with index 23 of it. So [line] is GPIO pin BCM 23.
|
||||
///
|
||||
/// The main GPIO chip is called `pinctrl-bcm2711` on Pi 4 and `pinctrl-bcm2835`
|
||||
/// on older Raspberry Pis and it was also called that way on Pi 4 with older
|
||||
/// kernel versions.
|
||||
final chip = _chips.singleWhere(
|
||||
(chip) => chip.label == 'pinctrl-bcm2711',
|
||||
orElse: () =>
|
||||
_chips.singleWhere((chip) => chip.label == 'pinctrl-bcm2835'),
|
||||
);
|
||||
return chip;
|
||||
}
|
||||
|
||||
// International Morse Code
|
||||
final Map<String, String> _morseAlphabet = {
|
||||
'A': '.-',
|
||||
'B': '-...',
|
||||
'C': '-.-.',
|
||||
'D': '-..',
|
||||
'E': '.',
|
||||
'F': '..-.',
|
||||
'G': '--.',
|
||||
'H': '....',
|
||||
'I': '..',
|
||||
'J': '.---',
|
||||
'K': '-.-',
|
||||
'L': '.-..',
|
||||
'M': '--',
|
||||
'N': '-.',
|
||||
'O': '---',
|
||||
'P': '.--.',
|
||||
'Q': '--.-',
|
||||
'R': '.-.',
|
||||
'S': '...',
|
||||
'T': '-',
|
||||
'U': '..-',
|
||||
'V': '...-',
|
||||
'W': '.--',
|
||||
'X': '-..-',
|
||||
'Y': '-.--',
|
||||
'Z': '--..',
|
||||
'0': '-----',
|
||||
'1': '.----',
|
||||
'2': '..---',
|
||||
'3': '...--',
|
||||
'4': '....-',
|
||||
'5': '.....',
|
||||
'6': '-....',
|
||||
'7': '--...',
|
||||
'8': '---..',
|
||||
'9': '----.',
|
||||
};
|
||||
}
|
57
lib/features/morse_led/presentation/led_screen.dart
Normal file
@ -0,0 +1,57 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../common/widgets/text_input_dialog.dart';
|
||||
import '../business/service/morse_service.dart';
|
||||
|
||||
class LedScreen extends ConsumerWidget {
|
||||
const LedScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final TextEditingController textController = TextEditingController();
|
||||
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
height: 224,
|
||||
child: Card(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.3),
|
||||
elevation: 0,
|
||||
margin: const EdgeInsets.all(32),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
child: TextInputDialog(
|
||||
controller: textController,
|
||||
title: 'Enter Morse Code Message',
|
||||
onReturnPress: () {
|
||||
// FocusScope.of(context).unfocus();
|
||||
context.pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
MorseService().printMessage(textController.text);
|
||||
},
|
||||
child: const Text("Morse Code"),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
45
lib/features/news_api/business/news_controller.dart
Normal file
@ -0,0 +1,45 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../data/repository/news_repository.dart';
|
||||
import '../data/models/region_news.dart';
|
||||
|
||||
part 'news_controller.g.dart';
|
||||
|
||||
/// The news controller
|
||||
@riverpod
|
||||
class NewsController extends _$NewsController {
|
||||
@override
|
||||
Future<RegionNews> build() async {
|
||||
try {
|
||||
final String newsResponse = await NewsRepository().fetchNews();
|
||||
final Map<String, dynamic> jsonData = json.decode(newsResponse);
|
||||
final RegionNews news = RegionNews.fromJson(jsonData);
|
||||
return news;
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Append the next page of news to the current news list
|
||||
Future<void> appendPage() async {
|
||||
try {
|
||||
final RegionNews currentNews =
|
||||
state.asData?.value ?? const RegionNews(news: []);
|
||||
final newsResponse =
|
||||
await NewsRepository().fetchNewsPage(currentNews.nextPage ?? 'null');
|
||||
final Map<String, dynamic> jsonData = jsonDecode(newsResponse);
|
||||
final RegionNews nextNews = RegionNews.fromJson(jsonData);
|
||||
final RegionNews appendNews =
|
||||
currentNews.copyWith(nextPage: nextNews.nextPage, news: [
|
||||
...currentNews.news,
|
||||
...nextNews.news,
|
||||
]);
|
||||
state = AsyncData(appendNews);
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
28
lib/features/news_api/business/news_controller.g.dart
Normal file
@ -0,0 +1,28 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'news_controller.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$newsControllerHash() => r'd2b544008f72eeee9a71bec7d0fe5b8981536fee';
|
||||
|
||||
/// The news controller
|
||||
///
|
||||
/// Copied from [NewsController].
|
||||
@ProviderFor(NewsController)
|
||||
final newsControllerProvider =
|
||||
AutoDisposeAsyncNotifierProvider<NewsController, RegionNews>.internal(
|
||||
NewsController.new,
|
||||
name: r'newsControllerProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$newsControllerHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$NewsController = AutoDisposeAsyncNotifier<RegionNews>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
15
lib/features/news_api/business/scroll_state_controller.dart
Normal file
@ -0,0 +1,15 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'scroll_state_controller.g.dart';
|
||||
|
||||
@riverpod
|
||||
class ScrollStateController extends _$ScrollStateController {
|
||||
@override
|
||||
bool build() {
|
||||
return false;
|
||||
}
|
||||
|
||||
toggle(bool set) {
|
||||
state = set;
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'scroll_state_controller.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$scrollStateControllerHash() =>
|
||||
r'0fd7eab4471ba8adac0d631c0580afb88e31901d';
|
||||
|
||||
/// See also [ScrollStateController].
|
||||
@ProviderFor(ScrollStateController)
|
||||
final scrollStateControllerProvider =
|
||||
AutoDisposeNotifierProvider<ScrollStateController, bool>.internal(
|
||||
ScrollStateController.new,
|
||||
name: r'scrollStateControllerProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$scrollStateControllerHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$ScrollStateController = AutoDisposeNotifier<bool>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
134
lib/features/news_api/data/models/news.dart
Normal file
@ -0,0 +1,134 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import 'teaser_image.dart';
|
||||
|
||||
part 'news.freezed.dart';
|
||||
part 'news.g.dart';
|
||||
|
||||
@freezed
|
||||
class News with _$News {
|
||||
const factory News({
|
||||
String? title,
|
||||
TeaserImage? teaserImage,
|
||||
TeaserImage? brandingImage,
|
||||
String? date,
|
||||
String? type,
|
||||
}) = _News;
|
||||
|
||||
factory News.fromJson(Map<String, dynamic> json) => _$NewsFromJson(json);
|
||||
}
|
||||
|
||||
// old manual implementation:
|
||||
|
||||
// final String sophoraId;
|
||||
// final String externalId;
|
||||
// final String title;
|
||||
// final TeaserImage? teaserImage;
|
||||
// final TeaserImage? brandingImage;
|
||||
// final String date;
|
||||
// final String updateCheckUrl;
|
||||
// final int regionId;
|
||||
// final String details;
|
||||
// final String detailsweb;
|
||||
// final String shareURL;
|
||||
// final String topline;
|
||||
// final String firstSentence;
|
||||
// final String type;
|
||||
// News({
|
||||
// required this.sophoraId,
|
||||
// required this.externalId,
|
||||
// required this.title,
|
||||
// this.teaserImage,
|
||||
// this.brandingImage,
|
||||
// required this.date,
|
||||
// required this.updateCheckUrl,
|
||||
// required this.regionId,
|
||||
// required this.details,
|
||||
// required this.detailsweb,
|
||||
// required this.shareURL,
|
||||
// required this.topline,
|
||||
// required this.firstSentence,
|
||||
// required this.type,
|
||||
// });
|
||||
|
||||
// News copyWith({
|
||||
// String? sophoraId,
|
||||
// String? externalId,
|
||||
// String? title,
|
||||
// TeaserImage? teaserImage,
|
||||
// TeaserImage? brandingImage,
|
||||
// String? date,
|
||||
// String? updateCheckUrl,
|
||||
// int? regionId,
|
||||
// String? details,
|
||||
// String? detailsweb,
|
||||
// String? shareURL,
|
||||
// String? topline,
|
||||
// String? firstSentence,
|
||||
// String? type,
|
||||
// }) {
|
||||
// return News(
|
||||
// sophoraId: sophoraId ?? this.sophoraId,
|
||||
// externalId: externalId ?? this.externalId,
|
||||
// title: title ?? this.title,
|
||||
// teaserImage: teaserImage ?? this.teaserImage,
|
||||
// brandingImage: brandingImage ?? this.brandingImage,
|
||||
// date: date ?? this.date,
|
||||
// updateCheckUrl: updateCheckUrl ?? this.updateCheckUrl,
|
||||
// regionId: regionId ?? this.regionId,
|
||||
// details: details ?? this.details,
|
||||
// detailsweb: detailsweb ?? this.detailsweb,
|
||||
// shareURL: shareURL ?? this.shareURL,
|
||||
// topline: topline ?? this.topline,
|
||||
// firstSentence: firstSentence ?? this.firstSentence,
|
||||
// type: type ?? this.type,
|
||||
// );
|
||||
// }
|
||||
|
||||
// Map<String, dynamic> toMap() {
|
||||
// return {
|
||||
// 'sophoraId': sophoraId,
|
||||
// 'externalId': externalId,
|
||||
// 'title': title,
|
||||
// 'teaserImage': teaserImage?.toMap(),
|
||||
// 'brandingImage': brandingImage?.toMap(),
|
||||
// 'date': date,
|
||||
// 'updateCheckUrl': updateCheckUrl,
|
||||
// 'regionId': regionId,
|
||||
// 'details': details,
|
||||
// 'detailsweb': detailsweb,
|
||||
// 'shareURL': shareURL,
|
||||
// 'topline': topline,
|
||||
// 'firstSentence': firstSentence,
|
||||
// 'type': type,
|
||||
// };
|
||||
// }
|
||||
|
||||
// factory News.fromMap(Map<String, dynamic> map) {
|
||||
// return News(
|
||||
// sophoraId: map['sophoraId'] ?? '',
|
||||
// externalId: map['externalId'] ?? '',
|
||||
// title: map['title'] ?? '',
|
||||
// teaserImage: map['teaserImage'] != null
|
||||
// ? TeaserImage.fromMap(map['teaserImage'])
|
||||
// : null,
|
||||
// brandingImage: map['brandingImage'] != null
|
||||
// ? TeaserImage.fromMap(map['brandingImage'])
|
||||
// : null,
|
||||
// date: map['date'] ?? '',
|
||||
// updateCheckUrl: map['updateCheckUrl'] ?? '',
|
||||
// regionId: map['regionId']?.toInt() ?? 0,
|
||||
// details: map['details'] ?? '',
|
||||
// detailsweb: map['detailsweb'] ?? '',
|
||||
// shareURL: map['shareURL'] ?? '',
|
||||
// topline: map['topline'] ?? '',
|
||||
// firstSentence: map['firstSentence'] ?? '',
|
||||
// type: map['type'] ?? '',
|
||||
// );
|
||||
// }
|
||||
|
||||
// String toJson() => json.encode(toMap());
|
||||
|
||||
// factory News.fromJson(String source) => News.fromMap(json.decode(source));
|
||||
// }
|
273
lib/features/news_api/data/models/news.freezed.dart
Normal file
@ -0,0 +1,273 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'news.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
News _$NewsFromJson(Map<String, dynamic> json) {
|
||||
return _News.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$News {
|
||||
String? get title => throw _privateConstructorUsedError;
|
||||
TeaserImage? get teaserImage => throw _privateConstructorUsedError;
|
||||
TeaserImage? get brandingImage => throw _privateConstructorUsedError;
|
||||
String? get date => throw _privateConstructorUsedError;
|
||||
String? get type => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$NewsCopyWith<News> get copyWith => throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $NewsCopyWith<$Res> {
|
||||
factory $NewsCopyWith(News value, $Res Function(News) then) =
|
||||
_$NewsCopyWithImpl<$Res, News>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{String? title,
|
||||
TeaserImage? teaserImage,
|
||||
TeaserImage? brandingImage,
|
||||
String? date,
|
||||
String? type});
|
||||
|
||||
$TeaserImageCopyWith<$Res>? get teaserImage;
|
||||
$TeaserImageCopyWith<$Res>? get brandingImage;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$NewsCopyWithImpl<$Res, $Val extends News>
|
||||
implements $NewsCopyWith<$Res> {
|
||||
_$NewsCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? title = freezed,
|
||||
Object? teaserImage = freezed,
|
||||
Object? brandingImage = freezed,
|
||||
Object? date = freezed,
|
||||
Object? type = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
title: freezed == title
|
||||
? _value.title
|
||||
: title // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
teaserImage: freezed == teaserImage
|
||||
? _value.teaserImage
|
||||
: teaserImage // ignore: cast_nullable_to_non_nullable
|
||||
as TeaserImage?,
|
||||
brandingImage: freezed == brandingImage
|
||||
? _value.brandingImage
|
||||
: brandingImage // ignore: cast_nullable_to_non_nullable
|
||||
as TeaserImage?,
|
||||
date: freezed == date
|
||||
? _value.date
|
||||
: date // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
type: freezed == type
|
||||
? _value.type
|
||||
: type // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
) as $Val);
|
||||
}
|
||||
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$TeaserImageCopyWith<$Res>? get teaserImage {
|
||||
if (_value.teaserImage == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $TeaserImageCopyWith<$Res>(_value.teaserImage!, (value) {
|
||||
return _then(_value.copyWith(teaserImage: value) as $Val);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$TeaserImageCopyWith<$Res>? get brandingImage {
|
||||
if (_value.brandingImage == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $TeaserImageCopyWith<$Res>(_value.brandingImage!, (value) {
|
||||
return _then(_value.copyWith(brandingImage: value) as $Val);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$NewsImplCopyWith<$Res> implements $NewsCopyWith<$Res> {
|
||||
factory _$$NewsImplCopyWith(
|
||||
_$NewsImpl value, $Res Function(_$NewsImpl) then) =
|
||||
__$$NewsImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{String? title,
|
||||
TeaserImage? teaserImage,
|
||||
TeaserImage? brandingImage,
|
||||
String? date,
|
||||
String? type});
|
||||
|
||||
@override
|
||||
$TeaserImageCopyWith<$Res>? get teaserImage;
|
||||
@override
|
||||
$TeaserImageCopyWith<$Res>? get brandingImage;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$NewsImplCopyWithImpl<$Res>
|
||||
extends _$NewsCopyWithImpl<$Res, _$NewsImpl>
|
||||
implements _$$NewsImplCopyWith<$Res> {
|
||||
__$$NewsImplCopyWithImpl(_$NewsImpl _value, $Res Function(_$NewsImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? title = freezed,
|
||||
Object? teaserImage = freezed,
|
||||
Object? brandingImage = freezed,
|
||||
Object? date = freezed,
|
||||
Object? type = freezed,
|
||||
}) {
|
||||
return _then(_$NewsImpl(
|
||||
title: freezed == title
|
||||
? _value.title
|
||||
: title // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
teaserImage: freezed == teaserImage
|
||||
? _value.teaserImage
|
||||
: teaserImage // ignore: cast_nullable_to_non_nullable
|
||||
as TeaserImage?,
|
||||
brandingImage: freezed == brandingImage
|
||||
? _value.brandingImage
|
||||
: brandingImage // ignore: cast_nullable_to_non_nullable
|
||||
as TeaserImage?,
|
||||
date: freezed == date
|
||||
? _value.date
|
||||
: date // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
type: freezed == type
|
||||
? _value.type
|
||||
: type // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$NewsImpl with DiagnosticableTreeMixin implements _News {
|
||||
const _$NewsImpl(
|
||||
{this.title, this.teaserImage, this.brandingImage, this.date, this.type});
|
||||
|
||||
factory _$NewsImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$NewsImplFromJson(json);
|
||||
|
||||
@override
|
||||
final String? title;
|
||||
@override
|
||||
final TeaserImage? teaserImage;
|
||||
@override
|
||||
final TeaserImage? brandingImage;
|
||||
@override
|
||||
final String? date;
|
||||
@override
|
||||
final String? type;
|
||||
|
||||
@override
|
||||
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
||||
return 'News(title: $title, teaserImage: $teaserImage, brandingImage: $brandingImage, date: $date, type: $type)';
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty('type', 'News'))
|
||||
..add(DiagnosticsProperty('title', title))
|
||||
..add(DiagnosticsProperty('teaserImage', teaserImage))
|
||||
..add(DiagnosticsProperty('brandingImage', brandingImage))
|
||||
..add(DiagnosticsProperty('date', date))
|
||||
..add(DiagnosticsProperty('type', type));
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$NewsImpl &&
|
||||
(identical(other.title, title) || other.title == title) &&
|
||||
(identical(other.teaserImage, teaserImage) ||
|
||||
other.teaserImage == teaserImage) &&
|
||||
(identical(other.brandingImage, brandingImage) ||
|
||||
other.brandingImage == brandingImage) &&
|
||||
(identical(other.date, date) || other.date == date) &&
|
||||
(identical(other.type, type) || other.type == type));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, title, teaserImage, brandingImage, date, type);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$NewsImplCopyWith<_$NewsImpl> get copyWith =>
|
||||
__$$NewsImplCopyWithImpl<_$NewsImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$NewsImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _News implements News {
|
||||
const factory _News(
|
||||
{final String? title,
|
||||
final TeaserImage? teaserImage,
|
||||
final TeaserImage? brandingImage,
|
||||
final String? date,
|
||||
final String? type}) = _$NewsImpl;
|
||||
|
||||
factory _News.fromJson(Map<String, dynamic> json) = _$NewsImpl.fromJson;
|
||||
|
||||
@override
|
||||
String? get title;
|
||||
@override
|
||||
TeaserImage? get teaserImage;
|
||||
@override
|
||||
TeaserImage? get brandingImage;
|
||||
@override
|
||||
String? get date;
|
||||
@override
|
||||
String? get type;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$NewsImplCopyWith<_$NewsImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
28
lib/features/news_api/data/models/news.g.dart
Normal file
@ -0,0 +1,28 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'news.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$NewsImpl _$$NewsImplFromJson(Map<String, dynamic> json) => _$NewsImpl(
|
||||
title: json['title'] as String?,
|
||||
teaserImage: json['teaserImage'] == null
|
||||
? null
|
||||
: TeaserImage.fromJson(json['teaserImage'] as Map<String, dynamic>),
|
||||
brandingImage: json['brandingImage'] == null
|
||||
? null
|
||||
: TeaserImage.fromJson(json['brandingImage'] as Map<String, dynamic>),
|
||||
date: json['date'] as String?,
|
||||
type: json['type'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$NewsImplToJson(_$NewsImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'title': instance.title,
|
||||
'teaserImage': instance.teaserImage,
|
||||
'brandingImage': instance.brandingImage,
|
||||
'date': instance.date,
|
||||
'type': instance.type,
|
||||
};
|
94
lib/features/news_api/data/models/region_news.dart
Normal file
@ -0,0 +1,94 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import 'news.dart';
|
||||
|
||||
part 'region_news.freezed.dart';
|
||||
part 'region_news.g.dart';
|
||||
|
||||
@freezed
|
||||
class RegionNews with _$RegionNews {
|
||||
const factory RegionNews({
|
||||
required List<News> news,
|
||||
String? nextPage,
|
||||
}) = _RegionNews;
|
||||
|
||||
factory RegionNews.fromJson(Map<String, dynamic> json) =>
|
||||
_$RegionNewsFromJson(json);
|
||||
}
|
||||
|
||||
// old manual implementation:
|
||||
|
||||
// class RegionNews {
|
||||
// List<News> news;
|
||||
// String newStoriesCountLink;
|
||||
// String type;
|
||||
// String? nextPage;
|
||||
// RegionNews({
|
||||
// required this.news,
|
||||
// required this.newStoriesCountLink,
|
||||
// required this.type,
|
||||
// this.nextPage,
|
||||
// });
|
||||
|
||||
// RegionNews copyWith({
|
||||
// List<News>? news,
|
||||
// String? newStoriesCountLink,
|
||||
// String? type,
|
||||
// String? nextPage,
|
||||
// }) {
|
||||
// return RegionNews(
|
||||
// news: news ?? this.news,
|
||||
// newStoriesCountLink: newStoriesCountLink ?? this.newStoriesCountLink,
|
||||
// type: type ?? this.type,
|
||||
// nextPage: nextPage ?? this.nextPage,
|
||||
// );
|
||||
// }
|
||||
|
||||
// Map<String, dynamic> toMap() {
|
||||
// return {
|
||||
// 'news': news.map((x) => x.toMap()).toList(),
|
||||
// 'newStoriesCountLink': newStoriesCountLink,
|
||||
// 'type': type,
|
||||
// 'nextPage': nextPage,
|
||||
// };
|
||||
// }
|
||||
|
||||
// factory RegionNews.fromMap(Map<String, dynamic> map) {
|
||||
// return RegionNews(
|
||||
// news: List<News>.from(map['news']?.map((x) => News.fromMap(x))),
|
||||
// newStoriesCountLink: map['newStoriesCountLink'] ?? '',
|
||||
// type: map['type'] ?? '',
|
||||
// nextPage: map['nextPage'],
|
||||
// );
|
||||
// }
|
||||
|
||||
// String toJson() => json.encode(toMap());
|
||||
|
||||
// factory RegionNews.fromJson(String source) =>
|
||||
// RegionNews.fromMap(json.decode(source));
|
||||
|
||||
// @override
|
||||
// String toString() {
|
||||
// return 'RegionNews(news: $news, newStoriesCountLink: $newStoriesCountLink, type: $type, nextPage: $nextPage)';
|
||||
// }
|
||||
|
||||
// @override
|
||||
// bool operator ==(Object other) {
|
||||
// if (identical(this, other)) return true;
|
||||
|
||||
// return other is RegionNews &&
|
||||
// listEquals(other.news, news) &&
|
||||
// other.newStoriesCountLink == newStoriesCountLink &&
|
||||
// other.type == type &&
|
||||
// other.nextPage == nextPage;
|
||||
// }
|
||||
|
||||
// @override
|
||||
// int get hashCode {
|
||||
// return news.hashCode ^
|
||||
// newStoriesCountLink.hashCode ^
|
||||
// type.hashCode ^
|
||||
// nextPage.hashCode;
|
||||
// }
|
||||
// }
|
187
lib/features/news_api/data/models/region_news.freezed.dart
Normal file
@ -0,0 +1,187 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'region_news.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
RegionNews _$RegionNewsFromJson(Map<String, dynamic> json) {
|
||||
return _RegionNews.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$RegionNews {
|
||||
List<News> get news => throw _privateConstructorUsedError;
|
||||
String? get nextPage => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$RegionNewsCopyWith<RegionNews> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $RegionNewsCopyWith<$Res> {
|
||||
factory $RegionNewsCopyWith(
|
||||
RegionNews value, $Res Function(RegionNews) then) =
|
||||
_$RegionNewsCopyWithImpl<$Res, RegionNews>;
|
||||
@useResult
|
||||
$Res call({List<News> news, String? nextPage});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$RegionNewsCopyWithImpl<$Res, $Val extends RegionNews>
|
||||
implements $RegionNewsCopyWith<$Res> {
|
||||
_$RegionNewsCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? news = null,
|
||||
Object? nextPage = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
news: null == news
|
||||
? _value.news
|
||||
: news // ignore: cast_nullable_to_non_nullable
|
||||
as List<News>,
|
||||
nextPage: freezed == nextPage
|
||||
? _value.nextPage
|
||||
: nextPage // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$RegionNewsImplCopyWith<$Res>
|
||||
implements $RegionNewsCopyWith<$Res> {
|
||||
factory _$$RegionNewsImplCopyWith(
|
||||
_$RegionNewsImpl value, $Res Function(_$RegionNewsImpl) then) =
|
||||
__$$RegionNewsImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({List<News> news, String? nextPage});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$RegionNewsImplCopyWithImpl<$Res>
|
||||
extends _$RegionNewsCopyWithImpl<$Res, _$RegionNewsImpl>
|
||||
implements _$$RegionNewsImplCopyWith<$Res> {
|
||||
__$$RegionNewsImplCopyWithImpl(
|
||||
_$RegionNewsImpl _value, $Res Function(_$RegionNewsImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? news = null,
|
||||
Object? nextPage = freezed,
|
||||
}) {
|
||||
return _then(_$RegionNewsImpl(
|
||||
news: null == news
|
||||
? _value._news
|
||||
: news // ignore: cast_nullable_to_non_nullable
|
||||
as List<News>,
|
||||
nextPage: freezed == nextPage
|
||||
? _value.nextPage
|
||||
: nextPage // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$RegionNewsImpl with DiagnosticableTreeMixin implements _RegionNews {
|
||||
const _$RegionNewsImpl({required final List<News> news, this.nextPage})
|
||||
: _news = news;
|
||||
|
||||
factory _$RegionNewsImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$RegionNewsImplFromJson(json);
|
||||
|
||||
final List<News> _news;
|
||||
@override
|
||||
List<News> get news {
|
||||
if (_news is EqualUnmodifiableListView) return _news;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_news);
|
||||
}
|
||||
|
||||
@override
|
||||
final String? nextPage;
|
||||
|
||||
@override
|
||||
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
||||
return 'RegionNews(news: $news, nextPage: $nextPage)';
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty('type', 'RegionNews'))
|
||||
..add(DiagnosticsProperty('news', news))
|
||||
..add(DiagnosticsProperty('nextPage', nextPage));
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$RegionNewsImpl &&
|
||||
const DeepCollectionEquality().equals(other._news, _news) &&
|
||||
(identical(other.nextPage, nextPage) ||
|
||||
other.nextPage == nextPage));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType, const DeepCollectionEquality().hash(_news), nextPage);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$RegionNewsImplCopyWith<_$RegionNewsImpl> get copyWith =>
|
||||
__$$RegionNewsImplCopyWithImpl<_$RegionNewsImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$RegionNewsImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _RegionNews implements RegionNews {
|
||||
const factory _RegionNews(
|
||||
{required final List<News> news,
|
||||
final String? nextPage}) = _$RegionNewsImpl;
|
||||
|
||||
factory _RegionNews.fromJson(Map<String, dynamic> json) =
|
||||
_$RegionNewsImpl.fromJson;
|
||||
|
||||
@override
|
||||
List<News> get news;
|
||||
@override
|
||||
String? get nextPage;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$RegionNewsImplCopyWith<_$RegionNewsImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
21
lib/features/news_api/data/models/region_news.g.dart
Normal file
@ -0,0 +1,21 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'region_news.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$RegionNewsImpl _$$RegionNewsImplFromJson(Map<String, dynamic> json) =>
|
||||
_$RegionNewsImpl(
|
||||
news: (json['news'] as List<dynamic>)
|
||||
.map((e) => News.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
nextPage: json['nextPage'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$RegionNewsImplToJson(_$RegionNewsImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'news': instance.news,
|
||||
'nextPage': instance.nextPage,
|
||||
};
|
62
lib/features/news_api/data/models/teaser_image.dart
Normal file
@ -0,0 +1,62 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'teaser_image.freezed.dart';
|
||||
part 'teaser_image.g.dart';
|
||||
|
||||
@freezed
|
||||
class TeaserImage with _$TeaserImage {
|
||||
const factory TeaserImage({
|
||||
Map<String, String>? imageVariants,
|
||||
}) = _TeaserImage;
|
||||
|
||||
factory TeaserImage.fromJson(Map<String, dynamic> json) =>
|
||||
_$TeaserImageFromJson(json);
|
||||
}
|
||||
|
||||
// old manual implementation:
|
||||
|
||||
// class TeaserImage {
|
||||
// final String? title;
|
||||
// final String alttext;
|
||||
// final String? copyright;
|
||||
// final String type;
|
||||
// final Map<String, String> imageVariants;
|
||||
|
||||
// TeaserImage({
|
||||
// this.title,
|
||||
// required this.alttext,
|
||||
// this.copyright,
|
||||
// required this.type,
|
||||
// required this.imageVariants,
|
||||
// });
|
||||
|
||||
// Map<String, dynamic> toMap() {
|
||||
// return {
|
||||
// 'title': title,
|
||||
// 'alttext': alttext,
|
||||
// 'copyright': copyright,
|
||||
// 'type': type,
|
||||
// 'imageVariants': Map.from(imageVariants),
|
||||
// };
|
||||
// }
|
||||
|
||||
// factory TeaserImage.fromMap(Map<String, dynamic> map) {
|
||||
// return TeaserImage(
|
||||
// title: map['title'] ?? '',
|
||||
// alttext: map['alttext'] ?? '',
|
||||
// copyright: map['copyright'],
|
||||
// type: map['type'] ?? '',
|
||||
// imageVariants: Map.from(map['imageVariants']),
|
||||
// );
|
||||
// }
|
||||
|
||||
// String toJson() => json.encode(toMap());
|
||||
|
||||
// factory TeaserImage.fromJson(String source) =>
|
||||
// TeaserImage.fromMap(json.decode(source));
|
||||
|
||||
// @override
|
||||
// String toString() {
|
||||
// return 'TeaserImage(title: $title, alttext: $alttext, copyright: $copyright, type: $type)';
|
||||
// }
|
||||
// }
|
162
lib/features/news_api/data/models/teaser_image.freezed.dart
Normal file
@ -0,0 +1,162 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'teaser_image.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
TeaserImage _$TeaserImageFromJson(Map<String, dynamic> json) {
|
||||
return _TeaserImage.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$TeaserImage {
|
||||
Map<String, String>? get imageVariants => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$TeaserImageCopyWith<TeaserImage> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $TeaserImageCopyWith<$Res> {
|
||||
factory $TeaserImageCopyWith(
|
||||
TeaserImage value, $Res Function(TeaserImage) then) =
|
||||
_$TeaserImageCopyWithImpl<$Res, TeaserImage>;
|
||||
@useResult
|
||||
$Res call({Map<String, String>? imageVariants});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$TeaserImageCopyWithImpl<$Res, $Val extends TeaserImage>
|
||||
implements $TeaserImageCopyWith<$Res> {
|
||||
_$TeaserImageCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? imageVariants = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
imageVariants: freezed == imageVariants
|
||||
? _value.imageVariants
|
||||
: imageVariants // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, String>?,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$TeaserImageImplCopyWith<$Res>
|
||||
implements $TeaserImageCopyWith<$Res> {
|
||||
factory _$$TeaserImageImplCopyWith(
|
||||
_$TeaserImageImpl value, $Res Function(_$TeaserImageImpl) then) =
|
||||
__$$TeaserImageImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({Map<String, String>? imageVariants});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$TeaserImageImplCopyWithImpl<$Res>
|
||||
extends _$TeaserImageCopyWithImpl<$Res, _$TeaserImageImpl>
|
||||
implements _$$TeaserImageImplCopyWith<$Res> {
|
||||
__$$TeaserImageImplCopyWithImpl(
|
||||
_$TeaserImageImpl _value, $Res Function(_$TeaserImageImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? imageVariants = freezed,
|
||||
}) {
|
||||
return _then(_$TeaserImageImpl(
|
||||
imageVariants: freezed == imageVariants
|
||||
? _value._imageVariants
|
||||
: imageVariants // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, String>?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$TeaserImageImpl implements _TeaserImage {
|
||||
const _$TeaserImageImpl({final Map<String, String>? imageVariants})
|
||||
: _imageVariants = imageVariants;
|
||||
|
||||
factory _$TeaserImageImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$TeaserImageImplFromJson(json);
|
||||
|
||||
final Map<String, String>? _imageVariants;
|
||||
@override
|
||||
Map<String, String>? get imageVariants {
|
||||
final value = _imageVariants;
|
||||
if (value == null) return null;
|
||||
if (_imageVariants is EqualUnmodifiableMapView) return _imageVariants;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(value);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'TeaserImage(imageVariants: $imageVariants)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$TeaserImageImpl &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._imageVariants, _imageVariants));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType, const DeepCollectionEquality().hash(_imageVariants));
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$TeaserImageImplCopyWith<_$TeaserImageImpl> get copyWith =>
|
||||
__$$TeaserImageImplCopyWithImpl<_$TeaserImageImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$TeaserImageImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _TeaserImage implements TeaserImage {
|
||||
const factory _TeaserImage({final Map<String, String>? imageVariants}) =
|
||||
_$TeaserImageImpl;
|
||||
|
||||
factory _TeaserImage.fromJson(Map<String, dynamic> json) =
|
||||
_$TeaserImageImpl.fromJson;
|
||||
|
||||
@override
|
||||
Map<String, String>? get imageVariants;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$TeaserImageImplCopyWith<_$TeaserImageImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
19
lib/features/news_api/data/models/teaser_image.g.dart
Normal file
@ -0,0 +1,19 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'teaser_image.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$TeaserImageImpl _$$TeaserImageImplFromJson(Map<String, dynamic> json) =>
|
||||
_$TeaserImageImpl(
|
||||
imageVariants: (json['imageVariants'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) => MapEntry(k, e as String),
|
||||
),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$TeaserImageImplToJson(_$TeaserImageImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'imageVariants': instance.imageVariants,
|
||||
};
|
62
lib/features/news_api/data/models/teaser_image_url.dart
Normal file
@ -0,0 +1,62 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'teaser_image_url.freezed.dart';
|
||||
part 'teaser_image_url.g.dart';
|
||||
|
||||
@freezed
|
||||
class TeaserImageUrl with _$TeaserImageUrl {
|
||||
const factory TeaserImageUrl({
|
||||
String? imageurl,
|
||||
}) = _TeaserImageUrl;
|
||||
|
||||
factory TeaserImageUrl.fromJson(Map<String, dynamic> json) =>
|
||||
_$TeaserImageUrlFromJson(json);
|
||||
}
|
||||
|
||||
// old manual implementation:
|
||||
|
||||
// class TeaserImageUrl {
|
||||
// final String imageurl;
|
||||
|
||||
// TeaserImageUrl({
|
||||
// required this.imageurl,
|
||||
// });
|
||||
|
||||
// TeaserImageUrl copyWith({
|
||||
// String? imageurl,
|
||||
// }) {
|
||||
// return TeaserImageUrl(
|
||||
// imageurl: imageurl ?? this.imageurl,
|
||||
// );
|
||||
// }
|
||||
|
||||
// Map<String, dynamic> toMap() {
|
||||
// return {
|
||||
// 'imageurl': imageurl,
|
||||
// };
|
||||
// }
|
||||
|
||||
// factory TeaserImageUrl.fromMap(Map<String, dynamic> map) {
|
||||
// return TeaserImageUrl(
|
||||
// imageurl: map['imageurl'] ?? '',
|
||||
// );
|
||||
// }
|
||||
|
||||
// String toJson() => json.encode(toMap());
|
||||
|
||||
// factory TeaserImageUrl.fromJson(String source) =>
|
||||
// TeaserImageUrl.fromMap(json.decode(source));
|
||||
|
||||
// @override
|
||||
// String toString() => 'TeaserImageUrl(imageurl: $imageurl)';
|
||||
|
||||
// @override
|
||||
// bool operator ==(Object other) {
|
||||
// if (identical(this, other)) return true;
|
||||
|
||||
// return other is TeaserImageUrl && other.imageurl == imageurl;
|
||||
// }
|
||||
|
||||
// @override
|
||||
// int get hashCode => imageurl.hashCode;
|
||||
// }
|
154
lib/features/news_api/data/models/teaser_image_url.freezed.dart
Normal file
@ -0,0 +1,154 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'teaser_image_url.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
TeaserImageUrl _$TeaserImageUrlFromJson(Map<String, dynamic> json) {
|
||||
return _TeaserImageUrl.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$TeaserImageUrl {
|
||||
String? get imageurl => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$TeaserImageUrlCopyWith<TeaserImageUrl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $TeaserImageUrlCopyWith<$Res> {
|
||||
factory $TeaserImageUrlCopyWith(
|
||||
TeaserImageUrl value, $Res Function(TeaserImageUrl) then) =
|
||||
_$TeaserImageUrlCopyWithImpl<$Res, TeaserImageUrl>;
|
||||
@useResult
|
||||
$Res call({String? imageurl});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$TeaserImageUrlCopyWithImpl<$Res, $Val extends TeaserImageUrl>
|
||||
implements $TeaserImageUrlCopyWith<$Res> {
|
||||
_$TeaserImageUrlCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? imageurl = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
imageurl: freezed == imageurl
|
||||
? _value.imageurl
|
||||
: imageurl // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$TeaserImageUrlImplCopyWith<$Res>
|
||||
implements $TeaserImageUrlCopyWith<$Res> {
|
||||
factory _$$TeaserImageUrlImplCopyWith(_$TeaserImageUrlImpl value,
|
||||
$Res Function(_$TeaserImageUrlImpl) then) =
|
||||
__$$TeaserImageUrlImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({String? imageurl});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$TeaserImageUrlImplCopyWithImpl<$Res>
|
||||
extends _$TeaserImageUrlCopyWithImpl<$Res, _$TeaserImageUrlImpl>
|
||||
implements _$$TeaserImageUrlImplCopyWith<$Res> {
|
||||
__$$TeaserImageUrlImplCopyWithImpl(
|
||||
_$TeaserImageUrlImpl _value, $Res Function(_$TeaserImageUrlImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? imageurl = freezed,
|
||||
}) {
|
||||
return _then(_$TeaserImageUrlImpl(
|
||||
imageurl: freezed == imageurl
|
||||
? _value.imageurl
|
||||
: imageurl // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$TeaserImageUrlImpl implements _TeaserImageUrl {
|
||||
const _$TeaserImageUrlImpl({this.imageurl});
|
||||
|
||||
factory _$TeaserImageUrlImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$TeaserImageUrlImplFromJson(json);
|
||||
|
||||
@override
|
||||
final String? imageurl;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'TeaserImageUrl(imageurl: $imageurl)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$TeaserImageUrlImpl &&
|
||||
(identical(other.imageurl, imageurl) ||
|
||||
other.imageurl == imageurl));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, imageurl);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$TeaserImageUrlImplCopyWith<_$TeaserImageUrlImpl> get copyWith =>
|
||||
__$$TeaserImageUrlImplCopyWithImpl<_$TeaserImageUrlImpl>(
|
||||
this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$TeaserImageUrlImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _TeaserImageUrl implements TeaserImageUrl {
|
||||
const factory _TeaserImageUrl({final String? imageurl}) =
|
||||
_$TeaserImageUrlImpl;
|
||||
|
||||
factory _TeaserImageUrl.fromJson(Map<String, dynamic> json) =
|
||||
_$TeaserImageUrlImpl.fromJson;
|
||||
|
||||
@override
|
||||
String? get imageurl;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$TeaserImageUrlImplCopyWith<_$TeaserImageUrlImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
18
lib/features/news_api/data/models/teaser_image_url.g.dart
Normal file
@ -0,0 +1,18 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'teaser_image_url.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$TeaserImageUrlImpl _$$TeaserImageUrlImplFromJson(Map<String, dynamic> json) =>
|
||||
_$TeaserImageUrlImpl(
|
||||
imageurl: json['imageurl'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$TeaserImageUrlImplToJson(
|
||||
_$TeaserImageUrlImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'imageurl': instance.imageurl,
|
||||
};
|