init-project #5

Merged
allanger merged 1 commits from init-project into main 2026-05-27 14:10:32 +00:00
76 changed files with 1961 additions and 117 deletions
Showing only changes of commit 09df205fdb - Show all commits

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
assets/** filter=lfs diff=lfs merge=lfs -text

View File

@@ -1 +1 @@
flutter 3.41.9-stable
flutter 3.44.0-stable

Binary file not shown.

Binary file not shown.

BIN
assets/fonts/inter/Inter-Bold.ttf LFS Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/fonts/inter/Inter-Thin.ttf LFS Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/fonts/syne/Syne-Bold.ttf LFS Normal file

Binary file not shown.

Binary file not shown.

BIN
assets/fonts/syne/Syne-Medium.ttf LFS Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,45 +0,0 @@
import 'dart:developer';
import 'package:grpc/grpc_web.dart';
import 'package:softplayer_dart_proto/src/test/v1/test_v1.pb.dart';
import 'package:softplayer_dart_proto/src/test/v1/test_v1.pbgrpc.dart';
class TestAuthGrpc {
final GrpcWebClientChannel channel;
late TestServiceClient serviceStub;
TestAuthGrpc({required this.channel});
void init() {
serviceStub = TestServiceClient(channel);
}
Future<void> pong() async {
final request = PongRequest();
try {
final response = await serviceStub.pong(request);
log(response.toString());
} catch (e) {
rethrow;
}
}
}
class TestNoAuthGrpc {
final GrpcWebClientChannel channel;
late PublicTestServiceClient serviceStub;
TestNoAuthGrpc({required this.channel});
void init() {
serviceStub = PublicTestServiceClient(channel);
}
Future<void> ping() async {
final request = PingRequest();
try {
final response = await serviceStub.ping(request);
log(response.toString());
} catch (e) {
rethrow;
}
}
}

View File

@@ -0,0 +1,14 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:grpc/grpc_web.dart';
import 'package:protobuf/well_known_types/google/protobuf/empty.pb.dart';
import 'package:softplayer_dart_proto/accounts/v1/accounts_v1.pbgrpc.dart';
import 'package:softplayer_web/core/grpc/grpc_client.dart';
final accountsGrpcProvider = Provider<AccountsGrpcRepository>((ref) {
return AccountsGrpcRepository(ref.watch(accountsServiceClientProvider));
});
class AccountsGrpcRepository {
AccountsGrpcRepository(this._client);
final AccountsServiceClient _client;
}

View File

@@ -0,0 +1,35 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:grpc/grpc_web.dart';
import 'package:softplayer_dart_proto/accounts/v1/accounts_v1.pbgrpc.dart';
import 'package:softplayer_web/core/grpc/grpc_client.dart';
final publicAccountsGrpcProvider = Provider<PublicAccountsGrpcRepository>((
ref,
) {
return PublicAccountsGrpcRepository(
ref.watch(publicAccountsServiceClientProvider),
);
});
class PublicAccountsGrpcRepository {
PublicAccountsGrpcRepository(this._client);
final PublicAccountsServiceClient _client;
ResponseFuture<SignInResponse> signIn(SignInRequest req) {
try {
final response = _client.signIn(req);
return response;
} catch (error) {
rethrow;
}
}
ResponseFuture<SignUpResponse> signUp(SignUpRequest req) {
try {
final response = _client.signUp(req);
return response;
} catch (error) {
rethrow;
}
}
}

View File

@@ -0,0 +1,28 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:grpc/grpc_web.dart';
import 'package:softplayer_dart_proto/accounts/v1/accounts_v1.pbgrpc.dart';
import 'package:softplayer_web/core/grpc/grpc_client.dart';
final refreshSessionGrpcProvider = Provider<RefreshSessionGrpcRepository>((
ref,
) {
return RefreshSessionGrpcRepository(
ref.watch(refreshSessionServiceClientProvider),
);
});
class RefreshSessionGrpcRepository {
final RefreshSessionServiceClient _client;
RefreshSessionGrpcRepository(this._client);
ResponseFuture<RefreshSessionResponse> refreshSession(String refreshToken) {
try {
final response = _client.refreshSession(
RefreshSessionRequest(refreshToken: refreshToken),
);
return response;
} catch (error) {
rethrow;
}
}
}

22
lib/core/api/v1/test.dart Normal file
View File

@@ -0,0 +1,22 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:grpc/grpc_web.dart';
import 'package:softplayer_dart_proto/test/v1/test_v1.pbgrpc.dart';
import 'package:softplayer_web/core/grpc/grpc_client.dart';
final testGrpcProvider = Provider<TestGrpcRepository>((ref) {
return TestGrpcRepository(ref.watch(testServiceClientProvider));
});
class TestGrpcRepository {
TestGrpcRepository(this._client);
final TestServiceClient _client;
ResponseFuture<PongResponse> pong() {
try {
final response = _client.pong(PongRequest());
return response;
} catch (error) {
rethrow;
}
}
}

View File

@@ -0,0 +1,71 @@
import 'dart:developer';
import 'package:grpc/grpc.dart';
class AuthInterceptor extends ClientInterceptor {
final String Function() getAccessToken;
final String Function() getRefreshToken;
AuthInterceptor({
required this.getAccessToken,
required this.getRefreshToken,
});
bool _isPublicEndpoint(String path) {
return path.contains('/Public');
}
bool _isRefreshEndpoint(String path) {
return path.contains('/RefreshToken');
}
Map<String, String> _buildMetadata(String path) {
final metadata = <String, String>{};
// Public endpoints → no token
if (_isPublicEndpoint(path)) {
log("Public endpoint, no tokens needed");
return metadata;
}
String? token;
// Refresh endpoint → use refresh token
if (_isRefreshEndpoint(path)) {
log("Adding a refresh token to the metadata");
token = getRefreshToken();
} else {
log("Adding an acces token to the metadata");
token = getAccessToken();
}
if (token.isNotEmpty) {
metadata['authorization'] = 'Bearer $token';
}
return metadata;
}
@override
ResponseStream<R> interceptStreaming<Q, R>(
ClientMethod<Q, R> method,
Stream<Q> requests,
CallOptions options,
ClientStreamingInvoker<Q, R> invoker,
) {
// TODO: implement interceptStreaming
throw UnimplementedError();
}
@override
ResponseFuture<R> interceptUnary<Q, R>(
ClientMethod<Q, R> method,
Q request,
CallOptions options,
ClientUnaryInvoker<Q, R> invoker,
) {
final modifiedOptions = options.mergedWith(
CallOptions(metadata: _buildMetadata(method.path)),
);
return super.interceptUnary(method, request, modifiedOptions, invoker);
}
}

View File

@@ -0,0 +1,16 @@
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:grpc/grpc_web.dart';
final grpcChannelProvider = Provider<GrpcWebClientChannel>((ref) {
String backendURL = dotenv.env['SOFTPLAYER_BACKEND_URL']!;
final GrpcWebClientChannel channel = GrpcWebClientChannel.xhr(
Uri.parse(backendURL),
);
ref.onDispose(() async {
await channel.shutdown();
});
return channel;
});

View File

@@ -0,0 +1,54 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:softplayer_dart_proto/accounts/v1/accounts_v1.pbgrpc.dart';
import 'package:softplayer_dart_proto/test/v1/test_v1.pbgrpc.dart';
import 'package:softplayer_web/core/grpc/grpc_auth_interceptor.dart';
import 'package:softplayer_web/core/tokens/application/tokens_application.dart';
import 'grpc_channel_provider.dart';
final testServiceClientProvider = Provider<TestServiceClient>((ref) {
final channel = ref.watch(grpcChannelProvider);
final tokenState = ref.read(tokensControllerProvider).value;
if (tokenState == null) {
throw Exception("Token state is not initialized");
}
return TestServiceClient(
channel,
interceptors: [
AuthInterceptor(
getAccessToken: tokenState.getAccessToken,
getRefreshToken: tokenState.getRefreshToken,
),
],
);
});
final accountsServiceClientProvider = Provider<AccountsServiceClient>((ref) {
final channel = ref.watch(grpcChannelProvider);
final tokenState = ref.read(tokensControllerProvider).value;
if (tokenState == null) {
throw Exception("Token state is not initialized");
}
return AccountsServiceClient(
channel,
interceptors: [
AuthInterceptor(
getAccessToken: tokenState.getAccessToken,
getRefreshToken: tokenState.getRefreshToken,
),
],
);
});
final publicAccountsServiceClientProvider =
Provider<PublicAccountsServiceClient>((ref) {
final channel = ref.watch(grpcChannelProvider);
return PublicAccountsServiceClient(channel);
});
final refreshSessionServiceClientProvider =
Provider<RefreshSessionServiceClient>((ref) {
final channel = ref.watch(grpcChannelProvider);
return RefreshSessionServiceClient(channel);
});

View File

@@ -0,0 +1,30 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:softplayer_web/core/tokens/application/tokens_application.dart';
import 'package:softplayer_web/features/authorization/application/authorization_application.dart';
import 'package:softplayer_web/features/authorization/presentation/authorization_page.dart';
import 'package:softplayer_web/features/dashboard/presentation/dashboard_page.dart';
final goRouterProvider = Provider<GoRouter>((ref) {
final authState = ref.watch(authorizationControllerProvider);
final _tokenCtrl = ref.watch(tokensControllerProvider);
// If not authorized, redirect to the auth page, otherwise dashboard
return GoRouter(
initialLocation: '/',
redirect: (context, state) {
final isAuthorized = authState.requireValue.isAuthorized;
if (!isAuthorized) {
return "/auth";
}
return "/";
},
routes: [
GoRoute(path: '/', builder: (context, state) => const DashboardPage()),
GoRoute(
path: '/auth',
builder: (context, state) => const AuthorizationPage(),
),
],
);
});

View File

@@ -0,0 +1,167 @@
import 'dart:developer';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:jwt_decoder/jwt_decoder.dart';
import 'package:softplayer_dart_proto/accounts/v1/accounts_v1.pb.dart';
import 'package:softplayer_web/core/api/v1/accounts.dart';
import 'package:softplayer_web/core/api/v1/refresh_session.dart';
import 'package:softplayer_web/core/grpc/grpc_client.dart';
import 'package:softplayer_web/core/tokens/data/token_storage_repository.dart';
import 'package:softplayer_web/features/authorization/application/authorization_application.dart';
class TokenState {
final String? accessToken;
final String? refreshToken;
const TokenState({this.accessToken, this.refreshToken});
TokenState copyWith({String? accessToken, String? refreshToken}) {
return TokenState(
refreshToken: refreshToken ?? this.refreshToken,
accessToken: accessToken ?? this.accessToken,
);
}
// Get an access token from the state
String getAccessToken() {
return accessToken ?? "";
}
// Get a refresh token from the state
String getRefreshToken() {
return refreshToken ?? "";
}
}
const accessTokenHeader = "x-access-token";
const refreshTokenHeader = "x-refresh-token";
class Tokens {
final String accessToken;
final String refreshToken;
const Tokens({required this.accessToken, required this.refreshToken});
factory Tokens.fromProto(TokenPair pair) {
return Tokens(
accessToken: pair.accessToken,
refreshToken: pair.refreshToken,
);
}
}
final tokensControllerProvider =
AsyncNotifierProvider<TokensController, TokenState>(TokensController.new);
class TokensController extends AsyncNotifier<TokenState> {
@override
Future<TokenState> build() async {
final tokenRepo = ref.read(tokenStorageRepositoryProvider);
final accessToken = await tokenRepo.getAccessToken();
final refreshToken = await tokenRepo.getRefreshToken();
// If refresh token is not valid, we just drop the session,
// even if there is an active access token, and return
// an empty state
if (!verifyJWT(refreshToken)) {
log("Refresh token is not valid, logging out");
await tokenRepo.clearStorage();
return TokenState();
}
// If access token is not valid, refresh it using the refresh token
if (verifyJWT(accessToken)) {
log("Access token is valid");
return TokenState(refreshToken: refreshToken, accessToken: accessToken);
}
log("Only refresh token is valid");
return TokenState(refreshToken: refreshToken);
}
Future<void> resetTokens() async {
final tokenRepo = ref.read(tokenStorageRepositoryProvider);
await tokenRepo.clearStorage();
state = await AsyncValue.guard(() async {
return TokenState();
});
}
// Store an access token to the storage and save it to the state
Future<void> writeAccessToken(String token) async {
final tokenRepo = ref.read(tokenStorageRepositoryProvider);
await tokenRepo.storeAccessToken(token);
state = await AsyncValue.guard(() async {
return state.value!.copyWith(accessToken: token);
});
}
// Store a pair of tokens to the storage and refresh the state
Future<void> writeTokenPair(Tokens pair) async {
final tokenRepo = ref.read(tokenStorageRepositoryProvider);
log("Writing tokens");
await tokenRepo.storeRefreshToken(pair.refreshToken);
await tokenRepo.storeAccessToken(pair.accessToken);
state = await AsyncValue.guard(() async {
return state.value!.copyWith(
accessToken: pair.accessToken,
refreshToken: pair.refreshToken,
);
});
}
Future<void> checkTokens() async {
final currentState = state.value;
if (currentState == null ||
currentState.accessToken == null ||
currentState.refreshToken == null) {
log("Trying to get tokens from the storage");
state = await AsyncValue.guard(() async {
final tokenRepo = ref.read(tokenStorageRepositoryProvider);
return TokenState(
accessToken: await tokenRepo.getAccessToken(),
refreshToken: await tokenRepo.getRefreshToken(),
);
});
}
log("checking tokens");
final inMemAccessToken = state.value!.getAccessToken();
// If we have a valid token in memory, nothing must be done
// TODO: Store the expiration in memory, to make it cheaper to check
if (verifyJWT(inMemAccessToken)) {
return;
} else {
log("Access it not valid");
final inMemRefreshToken = state.value!.getRefreshToken();
if (verifyJWT(inMemRefreshToken)) {
log("Trying to refresh");
try {
final refreshSessionGrpc = ref.read(refreshSessionGrpcProvider);
log("Trying to refresh");
final response = await refreshSessionGrpc.refreshSession(
inMemRefreshToken,
);
await writeTokenPair(Tokens.fromProto(response.tokenPair));
return;
} catch (e) {
log(e.toString());
}
}
}
log("Refrsh it not valid");
await resetTokens();
ref.read(authorizationControllerProvider.notifier).logout();
}
bool verifyJWT(String? token) {
if (token == null || token.isEmpty) {
return false;
}
try {
bool isExpired = JwtDecoder.isExpired(token);
return !isExpired;
} catch (_) {
rethrow;
}
}
}

View File

@@ -0,0 +1,42 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
const accessTokenKey = "access_token";
const refreshTokenKey = "refresh_token";
final tokenStorageRepositoryProvider = Provider<TokenStorageRepository>((ref) {
return TokenStorageRepository();
});
// Secure storage backend for storing tokens in the browser
class TokenStorageRepository {
static const _storage = FlutterSecureStorage();
TokenStorageRepository();
// Store the refresh token in the storage
Future<void> storeRefreshToken(String token) async {
await _storage.write(key: refreshTokenKey, value: token);
}
// Store the access token in the storage
Future<void> storeAccessToken(String token) async {
await _storage.write(key: accessTokenKey, value: token);
}
// Get the acccess token from the storage
Future<String?> getAccessToken() async {
final token = await _storage.read(key: accessTokenKey);
return token;
}
// Get the refresh token from the storage
Future<String?> getRefreshToken() async {
final token = await _storage.read(key: refreshTokenKey);
return token;
}
Future<void> clearStorage() async {
await _storage.delete(key: refreshTokenKey);
await _storage.delete(key: accessTokenKey);
}
}

View File

@@ -0,0 +1,90 @@
import 'dart:developer';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:softplayer_web/core/api/v1/public_accounts.dart';
import 'package:softplayer_web/core/tokens/application/tokens_application.dart';
import 'package:softplayer_web/features/authorization/application/sign_in_data.dart';
import 'package:softplayer_web/features/authorization/application/sign_up_data.dart';
class AuthState {
final AuthMode mode;
final bool isAuthorized;
const AuthState({this.mode = AuthMode.login, this.isAuthorized = false});
AuthState copyWith({AuthMode? mode, String? status, bool? isAuthorized}) {
return AuthState(
mode: mode ?? this.mode,
isAuthorized: isAuthorized ?? this.isAuthorized,
);
}
}
enum AuthMode { login, signup }
final authorizationControllerProvider =
AsyncNotifierProvider<AuthorizationController, AuthState>(
AuthorizationController.new,
);
class AuthorizationController extends AsyncNotifier<AuthState> {
@override
Future<AuthState> build() async {
// Use is considered authorized if tokens are set in the memory.
// In case tokens are not valid, it will be discovered by the first
// api call.
final tokenState = await ref.watch(tokensControllerProvider.future);
if (tokenState.getAccessToken().isEmpty &&
tokenState.getRefreshToken().isNotEmpty) {
final tokenCtrl = ref.read(tokensControllerProvider.notifier);
await tokenCtrl.checkTokens();
}
final isAuthorized =
tokenState.getAccessToken().isNotEmpty &&
tokenState.getRefreshToken().isNotEmpty;
return AuthState(isAuthorized: isAuthorized);
}
AuthMode authMode = AuthMode.login;
void toggleAuthMode() {
state = AsyncData(
state.value!.copyWith(
mode: state.value!.mode == AuthMode.login
? AuthMode.signup
: AuthMode.login,
),
);
}
Future<void> signin(SignInData form) async {
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
final accountsGrpc = ref.read(publicAccountsGrpcProvider);
final tokenCtrl = ref.read(tokensControllerProvider.notifier);
final response = await accountsGrpc.signIn(form.toProto());
await tokenCtrl.writeTokenPair(Tokens.fromProto(response.tokenPair));
return state.value!.copyWith(isAuthorized: true);
});
}
Future<void> signup(SignUpData form) async {
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
final accountsGrpc = ref.read(publicAccountsGrpcProvider);
final tokenCtrl = ref.read(tokensControllerProvider.notifier);
final response = await accountsGrpc.signUp(form.toProto());
await tokenCtrl.writeTokenPair(Tokens.fromProto(response.tokenPair));
return state.value!.copyWith(isAuthorized: true);
});
}
Future<void> logout() async {
state = await AsyncValue.guard(() async {
return state.value!.copyWith(isAuthorized: false);
});
}
}

View File

@@ -0,0 +1,12 @@
import 'package:softplayer_dart_proto/accounts/v1/accounts_v1.pbgrpc.dart';
class SignInData {
final String email;
final String password;
const SignInData({required this.email, required this.password});
SignInRequest toProto() {
return SignInRequest(email: email, password: password);
}
}

View File

@@ -0,0 +1,23 @@
import 'package:softplayer_dart_proto/accounts/v1/accounts_v1.pbgrpc.dart';
class SignUpData {
final String email;
final String password;
final String name;
final String surname;
const SignUpData({
required this.email,
required this.password,
required this.name,
required this.surname,
});
SignUpRequest toProto() {
return SignUpRequest(
email: email,
password: password,
personalData: PersonalData(name: name, surname: surname),
);
}
}

View File

@@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
class AuthDecoration extends StatelessWidget {
final double minSquareSize;
final double spacing;
final Color color;
const AuthDecoration({
super.key,
this.minSquareSize = 40,
this.spacing = 24, // ✅ default 24px gap
this.color = Colors.blue,
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final width = constraints.maxWidth;
// Step 1: decide number of columns (max 4)
int columns = 4;
double calcSize(int cols) {
final totalSpacing = spacing * (cols - 1);
return (width - totalSpacing) / cols;
}
while (columns > 1 && calcSize(columns) < minSquareSize) {
columns--;
}
final squareSize = calcSize(columns);
// Step 2: compute how many rows are needed to fill screen height
final height = constraints.maxHeight;
final rows = (height / (squareSize + spacing)).ceil();
return Column(
children: List.generate(rows, (row) {
return Padding(
padding: EdgeInsets.all(spacing),
child: Row(
children: List.generate(columns, (col) {
return Padding(
padding: EdgeInsets.only(
right: col == columns - 1 ? 0 : spacing,
),
child: SizedBox(
width: squareSize,
height: squareSize,
child: DecoratedBox(
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(6),
),
),
),
);
}),
),
);
}),
);
},
);
}
}

View File

@@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:softplayer_web/features/authorization/application/authorization_application.dart';
import 'package:softplayer_web/features/authorization/presentation/decoration.dart';
import 'package:softplayer_web/features/authorization/presentation/login_form.dart';
import 'package:softplayer_web/features/authorization/presentation/register_form.dart';
class AuthorizationPage extends ConsumerStatefulWidget {
const AuthorizationPage({super.key});
@override
ConsumerState<AuthorizationPage> createState() => _AuthorizationPage();
}
class _AuthorizationPage extends ConsumerState<AuthorizationPage> {
@override
Widget build(BuildContext context) {
final state = ref.watch(authorizationControllerProvider);
return state.when(
data: (value) {
return LayoutBuilder(
builder: (context, constraints) {
final showDecoration = constraints.maxWidth > 1000;
return Scaffold(
body: Row(
children: [
Expanded(
flex: showDecoration ? 7 : 1,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (value.mode == AuthMode.login)
LoginForm()
else
RegisterForm(),
],
),
),
),
// ✅ only show decoration if width > 400
if (showDecoration)
Expanded(flex: 3, child: AuthDecoration()),
],
),
);
},
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('$e')),
);
}
}

View File

@@ -0,0 +1,89 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:softplayer_web/shared/ui/colors/light_mode.dart';
class AuthDecoration extends StatelessWidget {
final double spacing;
final Color color;
const AuthDecoration({
super.key,
this.spacing = 24,
this.color = Colors.blue,
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
return Container(
color: LightModeColors.backgroundElevated,
child: FractionallySizedBox(
widthFactor: 0.3,
alignment: Alignment.centerLeft,
child: CustomPaint(
painter: _GridPainter(spacing: spacing, color: color),
child: const SizedBox.expand(),
),
),
);
},
);
}
}
// todo: Some AI generated shite, that doesn't work
class _GridPainter extends CustomPainter {
final double spacing;
final Color color;
_GridPainter({required this.spacing, required this.color});
static const double maxSquare = 85;
static const double minSquare = 48; // 2x gap
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = color;
// ✅ outer padding included in layout math
final innerWidth = size.width - spacing * 2;
final innerHeight = size.height - spacing * 2;
int columns = 10;
double calcSize(int cols) {
return (innerWidth - (cols - 1) * spacing) / cols;
}
// choose columns safely
while (columns > 1 && calcSize(columns) < minSquare) {
columns--;
}
double square = calcSize(columns);
square = math.min(square, maxSquare);
final rows = (innerHeight / (square + spacing)).ceil();
for (int row = 0; row < rows; row++) {
final y = spacing + row * (square + spacing);
for (int col = 0; col < columns; col++) {
final x = spacing + col * (square + spacing);
final rect = Rect.fromLTWH(x, y, square, square);
canvas.drawRRect(
RRect.fromRectAndRadius(rect, const Radius.circular(6)),
paint,
);
}
}
}
@override
bool shouldRepaint(covariant _GridPainter oldDelegate) {
return oldDelegate.spacing != spacing || oldDelegate.color != color;
}
}

View File

@@ -0,0 +1,144 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:softplayer_web/features/authorization/application/authorization_application.dart';
import 'package:softplayer_web/features/authorization/application/sign_in_data.dart';
class LoginForm extends ConsumerStatefulWidget {
const LoginForm({super.key});
@override
ConsumerState<LoginForm> createState() => _LoginForm();
}
class _LoginForm extends ConsumerState<LoginForm> {
final _formKey = GlobalKey<FormState>();
final TextEditingController emailCtrl = TextEditingController();
final TextEditingController passwordCtrl = TextEditingController();
void _submitForm() {
if (_formKey.currentState!.validate()) {
// If valid, you can use the values
final email = emailCtrl.text;
final password = passwordCtrl.text;
final form = SignInData(email: email, password: password);
ref.read(authorizationControllerProvider.notifier).signin(form);
}
}
@override
Widget build(BuildContext context) {
final state = ref.watch(authorizationControllerProvider);
final controller = ref.read(authorizationControllerProvider.notifier);
return SizedBox(
width: 400,
child: Form(
key: _formKey,
child: Column(
children: [
Container(
alignment: Alignment.topLeft,
child: SelectableText(
"Welcome back!",
style: Theme.of(context).textTheme.headlineLarge,
),
),
SizedBox(height: 12),
Container(
alignment: Alignment.topLeft,
child: Row(
children: [
Text(
"Don't have an account yet? ",
style: Theme.of(context).textTheme.bodyMedium,
),
TextButton(
onPressed: controller.toggleAuthMode,
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
minimumSize: Size(0, 0),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: const Text(
"Sign up now",
style: TextStyle(decoration: TextDecoration.underline),
),
),
],
),
),
SizedBox(height: 36),
TextFormField(
onFieldSubmitted: (_) => _submitForm(),
controller: emailCtrl,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Email is required';
}
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(value.trim())) {
return 'Enter a valid email address';
}
return null;
},
decoration: InputDecoration(hintText: "Email address"),
),
SizedBox(height: 16),
TextFormField(
onFieldSubmitted: (_) => _submitForm(),
controller: passwordCtrl,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Password is required';
}
if (value.length < 12) {
return 'Password must be at least 12 characters long';
}
final hasNumber = RegExp(r'\d');
final hasSpecialChar = RegExp(
r'[!@#$%^&*(),.?":{}|<>_\-\\[\]\\/+=~`]',
);
if (!hasNumber.hasMatch(value)) {
return 'Password must contain at least one number';
}
if (!hasSpecialChar.hasMatch(value)) {
return 'Password must contain at least one special character';
}
return null;
},
obscureText: true,
decoration: InputDecoration(hintText: "Password"),
),
SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: Text("Forgot password?", textAlign: TextAlign.left),
),
ElevatedButton(
onPressed: _submitForm,
child: const Text('Log in'),
// ) {
// if (_formKey.currentState!.validate()) {
// ScaffoldMessenger.of(context).showSnackBar(
// const SnackBar(content: Text('Processing Data')),
// );
// }
// _
// },
),
state.when(
loading: () => const CircularProgressIndicator(),
data: (_) => const SizedBox.shrink(),
error: (_, _) => const SizedBox.shrink(),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,105 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:softplayer_web/features/authorization/application/authorization_application.dart';
import 'package:softplayer_web/features/authorization/application/sign_up_data.dart';
class RegisterForm extends ConsumerStatefulWidget {
const RegisterForm({super.key});
@override
ConsumerState<RegisterForm> createState() => _RegisterForm();
}
class _RegisterForm extends ConsumerState<RegisterForm> {
final emailCtrl = TextEditingController();
final nameCtrl = TextEditingController();
final surnameCtrl = TextEditingController();
final passwordCtrl = TextEditingController();
final repeatPasswordCtrl = TextEditingController();
@override
Widget build(BuildContext context) {
final controller = ref.read(authorizationControllerProvider.notifier);
return SizedBox(
width: 400,
child: Column(
children: [
Container(
alignment: Alignment.topLeft,
child: SelectableText(
"Welcome!",
style: Theme.of(context).textTheme.headlineLarge,
),
),
SizedBox(height: 12),
Container(
alignment: Alignment.topLeft,
child: Row(
children: [
Text(
"Already have an account? ",
style: Theme.of(context).textTheme.bodyMedium,
),
TextButton(
onPressed: controller.toggleAuthMode,
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
minimumSize: Size(0, 0),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: const Text(
"Sign in",
style: TextStyle(
decoration: TextDecoration.underline,
color: Colors.blue,
),
),
),
],
),
),
SizedBox(height: 36),
TextField(
controller: nameCtrl,
decoration: InputDecoration(hintText: "Name"),
),
SizedBox(height: 16),
TextField(
controller: surnameCtrl,
decoration: InputDecoration(hintText: "Surname"),
),
SizedBox(height: 16),
TextField(
controller: emailCtrl,
decoration: InputDecoration(hintText: "Email address"),
),
SizedBox(height: 16),
TextField(
controller: passwordCtrl,
obscureText: true,
decoration: InputDecoration(hintText: "Password"),
),
SizedBox(height: 16),
TextField(
controller: repeatPasswordCtrl,
obscureText: true,
decoration: InputDecoration(hintText: "Repeat the password"),
),
TextButton(
onPressed: () {
final form = SignUpData(
email: emailCtrl.text,
password: passwordCtrl.text,
name: nameCtrl.text,
surname: surnameCtrl.text,
);
ref.read(authorizationControllerProvider.notifier).signup(form);
},
child: const Text('Sign up'),
),
],
),
);
}
}

View File

@@ -0,0 +1,30 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:softplayer_dart_proto/src/accounts/v1/accounts_v1.pbgrpc.dart';
import 'package:softplayer_web/features/authorization/data/public_accounts_grpc_repository.dart';
class DashboardState {
final bool authorized;
const DashboardState({this.authorized = false});
DashboardState copyWith({bool? authorized}) {
return DashboardState(authorized: authorized ?? this.authorized);
}
}
final dashboardControllerProvider =
AsyncNotifierProvider<DashboardController, DashboardState>(
DashboardController.new,
);
class DashboardController extends AsyncNotifier<DashboardState> {
static const _storage = FlutterSecureStorage();
@override
Future<DashboardState> build() async {
final accessToken = await _storage.read(key: "x-access-token");
if (accessToken == null || accessToken.isEmpty) {
return const DashboardState(authorized: false);
}
return const DashboardState(authorized: true);
}
}

View File

@@ -0,0 +1,31 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:softplayer_web/features/test/application/test_controller.dart';
class DashboardPage extends ConsumerStatefulWidget {
const DashboardPage({super.key});
@override
ConsumerState<DashboardPage> createState() => _DashboardPage();
}
class _DashboardPage extends ConsumerState<DashboardPage> {
bool debug = false;
@override
Widget build(BuildContext context) {
final ctrl = ref.read(pingControllerProvider.notifier);
log("test");
return Container(
color: Colors.black87,
child: ElevatedButton(
onPressed: () {
ctrl.ping();
},
child: Text("pong"),
),
);
}
}

View File

@@ -0,0 +1,28 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:softplayer_web/core/api/v1/test.dart';
import 'package:softplayer_web/core/tokens/application/tokens_application.dart';
final pingControllerProvider = AsyncNotifierProvider<PingController, String>(
PingController.new,
);
class PingController extends AsyncNotifier<String> {
@override
Future<String> build() async {
return 'Idle';
}
Future<void> ping() async {
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
final testGrpc = ref.read(testGrpcProvider);
final tokenCtrl = ref.read(tokensControllerProvider.notifier);
await tokenCtrl.checkTokens();
await testGrpc.pong();
return 'Ping successful';
});
}
}

View File

@@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../application/test_controller.dart';
class PingPage extends ConsumerWidget {
const PingPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(pingControllerProvider);
return Scaffold(
appBar: AppBar(title: const Text('gRPC Ping')),
body: Center(
child: state.when(
data: (value) => Text(value),
loading: () => const CircularProgressIndicator(),
error: (e, _) => Text('Error: $e'),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
ref.read(pingControllerProvider.notifier).ping();
},
child: const Icon(Icons.send),
),
);
}
}

View File

@@ -1,72 +1,28 @@
import 'dart:developer';
import 'package:softplayer_web/api/grpc/test_service.dart';
import 'package:web/web.dart' as web;
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter/material.dart';
import 'package:grpc/grpc_web.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:softplayer_web/shared/ui/theme/app_theme.dart';
import 'core/router/app_router.dart';
void main() async {
await dotenv.load(fileName: ".env");
String backendURL = dotenv.env['SOFTPLAYER_BACKEND_URL']!;
GrpcWebClientChannel grpcChannel = GrpcWebClientChannel.xhr(
Uri.parse(backendURL),
);
WidgetsFlutterBinding.ensureInitialized();
runApp(MyApp(channel: grpcChannel));
SemanticsBinding.instance.ensureSemantics();
await dotenv.load(fileName: '.env');
runApp(const ProviderScope(child: App()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key, required this.channel});
final GrpcWebClientChannel channel;
class App extends ConsumerWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: true,
title: 'Softplayer',
home: RootWidget(channel: channel),
);
}
}
class RootWidget extends StatefulWidget {
final GrpcWebClientChannel channel;
const RootWidget({super.key, required this.channel});
@override
State<StatefulWidget> createState() => _StateRootWidget();
}
class _StateRootWidget extends State<RootWidget> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: double.infinity,
height: double.infinity,
child: Column(
children: [
ElevatedButton(
onPressed: () async {
TestAuthGrpc grpc = TestAuthGrpc(channel: widget.channel);
grpc.init();
await grpc.pong();
},
child: Text("pong"),
),
ElevatedButton(
onPressed: () async {
TestNoAuthGrpc grpc = TestNoAuthGrpc(channel: widget.channel);
grpc.init();
await grpc.ping();
},
child: Text("ping"),
),
],
),
),
Widget build(BuildContext context, WidgetRef ref) {
final router = ref.watch(goRouterProvider);
return MaterialApp.router(
routerConfig: router,
theme: AppTheme.light(),
darkTheme: AppTheme.dark(),
themeMode: ThemeMode.light,
);
}
}

View File

@@ -0,0 +1,8 @@
import 'package:flutter/material.dart';
class DarkModeColors {
static const darkBackground = Color(0xFF121212);
static const darkSurface = Color(0xFF1E1E1E);
static const darkPrimary = Color(0xFF8B7CFF);
static const darkText = Color(0xFFEDEDED);
}

View File

@@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
class LightModeColors {
// Light theme
static const backgroundPrimary = Color(0xFFF6F7F9);
static const backgroundElevated = Color(0xFFEEF0F3);
static const lightSurface = Color(0xFFF5F5F5);
static const lightPrimary = Color(0xFF6C5CE7);
static const lightText = Color(0xFF1A1A1A);
// Inputs
static const inputBackground = Color(0xFFFFFFFF);
static const inputDefaultBorder = Color(0xFFD9D9D9);
static const inputFocusedBorder = Color(0xFFEFFF1A);
// Text
static const textPrimary = Color(0xFF14161A);
// Buttons
static const buttonPrimary = Color(0xFFEFFF1A);
}

View File

@@ -0,0 +1,109 @@
import 'package:flutter/material.dart';
import '../colors/dark_mode.dart';
import '../colors/light_mode.dart';
class AppTheme {
static ThemeData light() {
return ThemeData(
brightness: Brightness.light,
scaffoldBackgroundColor: LightModeColors.backgroundPrimary,
colorScheme: const ColorScheme.light(
primary: LightModeColors.lightPrimary,
surface: LightModeColors.backgroundPrimary,
),
textTheme: const TextTheme(
headlineLarge: TextStyle(
fontSize: 22.0,
fontWeight: FontWeight.w600,
color: LightModeColors.textPrimary,
fontFamily: "Syne",
),
headlineMedium: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
bodyMedium: TextStyle(
fontFamily: "Inter",
fontSize: 14,
fontWeight: FontWeight.normal,
color: LightModeColors.textPrimary,
),
),
useMaterial3: true,
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
textStyle: TextStyle(
fontSize: 14,
fontFamily: "Inter",
color: LightModeColors.textPrimary,
fontWeight: FontWeight.normal,
),
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
textStyle: TextStyle(
fontSize: 14,
fontFamily: "Inter",
color: LightModeColors.textPrimary,
fontWeight: FontWeight.w600,
),
foregroundColor: LightModeColors.textPrimary,
backgroundColor: LightModeColors.buttonPrimary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
elevation: 0.0,
),
),
inputDecorationTheme: InputDecorationTheme(
// isDense: true,
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10),
// Optional hard height limits
// constraints: BoxConstraints(minHeight: 48, maxHeight: 40),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(8.0)),
borderSide: BorderSide(
color: LightModeColors.inputFocusedBorder,
width: 1.0,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(8.0)),
borderSide: BorderSide(
color: LightModeColors.inputDefaultBorder,
width: 1.0,
),
),
fillColor: LightModeColors.inputBackground,
),
);
}
static ThemeData dark() {
return ThemeData(
brightness: Brightness.dark,
scaffoldBackgroundColor: DarkModeColors.darkBackground,
colorScheme: const ColorScheme.dark(
primary: DarkModeColors.darkPrimary,
surface: DarkModeColors.darkSurface,
),
textTheme: const TextTheme(
bodyMedium: TextStyle(color: DarkModeColors.darkText),
),
useMaterial3: true,
inputDecorationTheme: InputDecorationTheme(
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.greenAccent, width: 5.0),
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.red, width: 5.0),
),
),
);
}
}

View File

@@ -1,6 +1,30 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d"
url: "https://pub.dev"
source: hosted
version: "93.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b
url: "https://pub.dev"
source: hosted
version: "10.0.1"
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
@@ -25,6 +49,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.1"
cli_config:
dependency: transitive
description:
name: cli_config
sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec
url: "https://pub.dev"
source: hosted
version: "0.2.0"
clock:
dependency: transitive
description:
@@ -33,6 +65,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.2"
code_assets:
dependency: transitive
description:
name: code_assets
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
collection:
dependency: transitive
description:
@@ -41,6 +81,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.2"
coverage:
dependency: transitive
description:
name: coverage
sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d"
url: "https://pub.dev"
source: hosted
version: "1.15.0"
crypto:
dependency: transitive
description:
@@ -81,6 +137,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
ffi_leak_tracker:
dependency: transitive
description:
name: ffi_leak_tracker
sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97"
url: "https://pub.dev"
source: hosted
version: "0.1.2"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
fixnum:
dependency: transitive
description:
@@ -110,11 +190,96 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_riverpod:
dependency: "direct main"
description:
name: flutter_riverpod
sha256: "4e166be88e1dbbaa34a280bdb744aeae73b7ef25fdf8db7a3bb776760a3648e2"
url: "https://pub.dev"
source: hosted
version: "3.3.1"
flutter_secure_storage:
dependency: "direct main"
description:
name: flutter_secure_storage
sha256: "6848263f9744072d0977347c383fb8b57d9780319a6bf5238b5a2866a029de62"
url: "https://pub.dev"
source: hosted
version: "10.2.0"
flutter_secure_storage_darwin:
dependency: transitive
description:
name: flutter_secure_storage_darwin
sha256: "67cd1ff671add31dc13e45194398187a04bb63804b37fa47866afae296d73fcb"
url: "https://pub.dev"
source: hosted
version: "0.3.1"
flutter_secure_storage_linux:
dependency: transitive
description:
name: flutter_secure_storage_linux
sha256: "2b5c76dce569ab752d55a1cee6a2242bcc11fdba927078fb88c503f150767cda"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
flutter_secure_storage_platform_interface:
dependency: transitive
description:
name: flutter_secure_storage_platform_interface
sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
flutter_secure_storage_web:
dependency: transitive
description:
name: flutter_secure_storage_web
sha256: "073a62b3aeb866ab4ce795f960413948e51e5a42a9b0c8333b6daf5bb3208a1c"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
flutter_secure_storage_windows:
dependency: transitive
description:
name: flutter_secure_storage_windows
sha256: "8f42f359f187a94dce7a3ab2ec5903d013dddfc7127078ebab19fa244c3840e8"
url: "https://pub.dev"
source: hosted
version: "4.2.1"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "4.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
go_router:
dependency: "direct main"
description:
name: go_router
sha256: "92d8cee7c57dff0a6c409c05597b460002434eccf7424a712283225b3962d03f"
url: "https://pub.dev"
source: hosted
version: "17.2.3"
google_cloud:
dependency: transitive
description:
@@ -147,6 +312,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.1.0"
hooks:
dependency: transitive
description:
name: hooks
sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e"
url: "https://pub.dev"
source: hosted
version: "1.0.3"
http:
dependency: "direct main"
description:
@@ -163,6 +336,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.1"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
url: "https://pub.dev"
source: hosted
version: "3.2.2"
http_parser:
dependency: transitive
description:
@@ -179,6 +360,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.5"
jni:
dependency: transitive
description:
name: jni
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
url: "https://pub.dev"
source: hosted
version: "1.0.0"
jni_flutter:
dependency: transitive
description:
name: jni_flutter
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
jwt_decoder:
dependency: "direct main"
description:
name: jwt_decoder
sha256: "54774aebf83f2923b99e6416b4ea915d47af3bde56884eb622de85feabbc559f"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
leak_tracker:
dependency: transitive
description:
@@ -211,6 +416,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.1.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher:
dependency: transitive
description:
@@ -231,10 +444,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
url: "https://pub.dev"
source: hosted
version: "1.17.0"
version: "1.18.0"
mime:
dependency: transitive
description:
@@ -243,6 +456,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.0"
native_toolchain_c:
dependency: transitive
description:
name: native_toolchain_c
sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572"
url: "https://pub.dev"
source: hosted
version: "0.17.6"
node_preamble:
dependency: transitive
description:
name: node_preamble
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
objective_c:
dependency: transitive
description:
name: objective_c
sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52"
url: "https://pub.dev"
source: hosted
version: "9.3.0"
package_config:
dependency: transitive
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.dev"
source: hosted
version: "2.2.0"
path:
dependency: transitive
description:
@@ -251,14 +496,110 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.1"
protobuf:
path_provider:
dependency: transitive
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
url: "https://pub.dev"
source: hosted
version: "2.6.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
pool:
dependency: transitive
description:
name: pool
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
url: "https://pub.dev"
source: hosted
version: "1.5.2"
protobuf:
dependency: "direct main"
description:
name: protobuf
sha256: "75ec242d22e950bdcc79ee38dd520ce4ee0bc491d7fadc4ea47694604d22bf06"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
record_use:
dependency: transitive
description:
name: record_use
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
riverpod:
dependency: transitive
description:
name: riverpod
sha256: "8c22216be8ad3ef2b44af3a329693558c98eca7b8bd4ef495c92db0bba279f83"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
shelf:
dependency: transitive
description:
@@ -267,6 +608,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.2"
shelf_packages_handler:
dependency: transitive
description:
name: shelf_packages_handler
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
shelf_static:
dependency: transitive
description:
name: shelf_static
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
url: "https://pub.dev"
source: hosted
version: "1.1.3"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
sky_engine:
dependency: transitive
description: flutter
@@ -276,11 +641,27 @@ packages:
dependency: "direct main"
description:
path: "."
ref: a9a7f05332596f776b37d2757b33f06f6faa92e9
resolved-ref: a9a7f05332596f776b37d2757b33f06f6faa92e9
ref: e801928517cee5298eb289f0993eec52e2186d86
resolved-ref: e801928517cee5298eb289f0993eec52e2186d86
url: "https://gitea.badhouseplants.net/softplayer/softplayer-dart-proto.git"
source: git
version: "1.0.0"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
url: "https://pub.dev"
source: hosted
version: "2.1.2"
source_maps:
dependency: transitive
description:
name: source_maps
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
url: "https://pub.dev"
source: hosted
version: "0.10.13"
source_span:
dependency: transitive
description:
@@ -297,6 +678,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.12.1"
state_notifier:
dependency: transitive
description:
name: state_notifier
sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb
url: "https://pub.dev"
source: hosted
version: "1.0.0"
stream_channel:
dependency: transitive
description:
@@ -321,14 +710,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test:
dependency: transitive
description:
name: test
sha256: "8d9ceddbab833f180fbefed08afa76d7c03513dfdba87ffcec2718b02bbcbf20"
url: "https://pub.dev"
source: hosted
version: "1.31.0"
test_api:
dependency: transitive
description:
name: test_api
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
url: "https://pub.dev"
source: hosted
version: "0.7.10"
version: "0.7.11"
test_core:
dependency: transitive
description:
name: test_core
sha256: "1991d4cfe85d5043241acac92962c3977c8d2f2add1ee73130c7b286417d1d34"
url: "https://pub.dev"
source: hosted
version: "0.6.17"
typed_data:
dependency: transitive
description:
@@ -353,6 +758,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "15.2.0"
watcher:
dependency: transitive
description:
name: watcher
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
web:
dependency: "direct main"
description:
@@ -361,6 +774,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.1"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
url: "https://pub.dev"
source: hosted
version: "3.0.3"
webkit_inspection_protocol:
dependency: transitive
description:
name: webkit_inspection_protocol
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
win32:
dependency: transitive
description:
name: win32
sha256: a1fc9eb9248baa05dfc12ed5b66e377b3e23f095eec078e0371622b9033810d9
url: "https://pub.dev"
source: hosted
version: "6.2.0"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.11.5 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"
flutter: ">=3.38.4"

View File

@@ -11,13 +11,18 @@ dependencies:
softplayer_dart_proto:
git:
url: https://gitea.badhouseplants.net/softplayer/softplayer-dart-proto.git
ref: a9a7f05332596f776b37d2757b33f06f6faa92e9
ref: e801928517cee5298eb289f0993eec52e2186d86
cupertino_icons: ^1.0.9
grpc: 5.1.0
http: ^1.6.0
dio: ^5.9.2
flutter_dotenv: ^6.0.1
web: ^1.1.1
flutter_riverpod: ^3.3.1
protobuf: ^6.0.0
go_router: ^17.2.3
flutter_secure_storage: ^10.2.0
jwt_decoder: ^2.0.1
dev_dependencies:
flutter_test:
sdk: flutter
@@ -27,3 +32,10 @@ flutter:
assets:
- assets/
- .env
fonts:
- family: Syne
fonts:
- asset: assets/fonts/syne/Syne-Regular.ttf
- family: Inter
fonts:
- asset: assets/fonts/inter/Inter-Regular.ttf