Start writing the web app
All checks were successful
ci/woodpecker/push/build Pipeline was successful
All checks were successful
ci/woodpecker/push/build Pipeline was successful
Signed-off-by: Nikolai Rodionov <allanger@posteo.com>
This commit was merged in pull request #5.
This commit is contained in:
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
assets/** filter=lfs diff=lfs merge=lfs -text
|
||||||
@@ -1 +1 @@
|
|||||||
flutter 3.41.9-stable
|
flutter 3.44.0-stable
|
||||||
|
|||||||
BIN
assets/fonts/inter/Inter-Black.ttf
LFS
Normal file
BIN
assets/fonts/inter/Inter-Black.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/Inter-BlackItalic.ttf
LFS
Normal file
BIN
assets/fonts/inter/Inter-BlackItalic.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/Inter-Bold.ttf
LFS
Normal file
BIN
assets/fonts/inter/Inter-Bold.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/Inter-BoldItalic.ttf
LFS
Normal file
BIN
assets/fonts/inter/Inter-BoldItalic.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/Inter-ExtraBold.ttf
LFS
Normal file
BIN
assets/fonts/inter/Inter-ExtraBold.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/Inter-ExtraBoldItalic.ttf
LFS
Normal file
BIN
assets/fonts/inter/Inter-ExtraBoldItalic.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/Inter-ExtraLight.ttf
LFS
Normal file
BIN
assets/fonts/inter/Inter-ExtraLight.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/Inter-ExtraLightItalic.ttf
LFS
Normal file
BIN
assets/fonts/inter/Inter-ExtraLightItalic.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/Inter-Italic.ttf
LFS
Normal file
BIN
assets/fonts/inter/Inter-Italic.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/Inter-Light.ttf
LFS
Normal file
BIN
assets/fonts/inter/Inter-Light.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/Inter-LightItalic.ttf
LFS
Normal file
BIN
assets/fonts/inter/Inter-LightItalic.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/Inter-Medium.ttf
LFS
Normal file
BIN
assets/fonts/inter/Inter-Medium.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/Inter-MediumItalic.ttf
LFS
Normal file
BIN
assets/fonts/inter/Inter-MediumItalic.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/Inter-Regular.ttf
LFS
Normal file
BIN
assets/fonts/inter/Inter-Regular.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/Inter-SemiBold.ttf
LFS
Normal file
BIN
assets/fonts/inter/Inter-SemiBold.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/Inter-SemiBoldItalic.ttf
LFS
Normal file
BIN
assets/fonts/inter/Inter-SemiBoldItalic.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/Inter-Thin.ttf
LFS
Normal file
BIN
assets/fonts/inter/Inter-Thin.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/Inter-ThinItalic.ttf
LFS
Normal file
BIN
assets/fonts/inter/Inter-ThinItalic.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/InterDisplay-Black.ttf
LFS
Normal file
BIN
assets/fonts/inter/InterDisplay-Black.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/InterDisplay-BlackItalic.ttf
LFS
Normal file
BIN
assets/fonts/inter/InterDisplay-BlackItalic.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/InterDisplay-Bold.ttf
LFS
Normal file
BIN
assets/fonts/inter/InterDisplay-Bold.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/InterDisplay-BoldItalic.ttf
LFS
Normal file
BIN
assets/fonts/inter/InterDisplay-BoldItalic.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/InterDisplay-ExtraBold.ttf
LFS
Normal file
BIN
assets/fonts/inter/InterDisplay-ExtraBold.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/InterDisplay-ExtraBoldItalic.ttf
LFS
Normal file
BIN
assets/fonts/inter/InterDisplay-ExtraBoldItalic.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/InterDisplay-ExtraLight.ttf
LFS
Normal file
BIN
assets/fonts/inter/InterDisplay-ExtraLight.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/InterDisplay-ExtraLightItalic.ttf
LFS
Normal file
BIN
assets/fonts/inter/InterDisplay-ExtraLightItalic.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/InterDisplay-Italic.ttf
LFS
Normal file
BIN
assets/fonts/inter/InterDisplay-Italic.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/InterDisplay-Light.ttf
LFS
Normal file
BIN
assets/fonts/inter/InterDisplay-Light.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/InterDisplay-LightItalic.ttf
LFS
Normal file
BIN
assets/fonts/inter/InterDisplay-LightItalic.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/InterDisplay-Medium.ttf
LFS
Normal file
BIN
assets/fonts/inter/InterDisplay-Medium.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/InterDisplay-MediumItalic.ttf
LFS
Normal file
BIN
assets/fonts/inter/InterDisplay-MediumItalic.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/InterDisplay-Regular.ttf
LFS
Normal file
BIN
assets/fonts/inter/InterDisplay-Regular.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/InterDisplay-SemiBold.ttf
LFS
Normal file
BIN
assets/fonts/inter/InterDisplay-SemiBold.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/InterDisplay-SemiBoldItalic.ttf
LFS
Normal file
BIN
assets/fonts/inter/InterDisplay-SemiBoldItalic.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/InterDisplay-Thin.ttf
LFS
Normal file
BIN
assets/fonts/inter/InterDisplay-Thin.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/InterDisplay-ThinItalic.ttf
LFS
Normal file
BIN
assets/fonts/inter/InterDisplay-ThinItalic.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/InterVariable-Italic.ttf
LFS
Normal file
BIN
assets/fonts/inter/InterVariable-Italic.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/inter/InterVariable.ttf
LFS
Normal file
BIN
assets/fonts/inter/InterVariable.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/syne/Syne-Bold.ttf
LFS
Normal file
BIN
assets/fonts/syne/Syne-Bold.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/syne/Syne-ExtraBold.ttf
LFS
Normal file
BIN
assets/fonts/syne/Syne-ExtraBold.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/syne/Syne-Medium.ttf
LFS
Normal file
BIN
assets/fonts/syne/Syne-Medium.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/syne/Syne-Regular.ttf
LFS
Normal file
BIN
assets/fonts/syne/Syne-Regular.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/syne/Syne-SemiBold.ttf
LFS
Normal file
BIN
assets/fonts/syne/Syne-SemiBold.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/syne/SyneMono-Regular.ttf
LFS
Normal file
BIN
assets/fonts/syne/SyneMono-Regular.ttf
LFS
Normal file
Binary file not shown.
BIN
assets/fonts/syne/SyneTactile-Regular.ttf
LFS
Normal file
BIN
assets/fonts/syne/SyneTactile-Regular.ttf
LFS
Normal file
Binary file not shown.
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
14
lib/core/api/v1/accounts.dart
Normal file
14
lib/core/api/v1/accounts.dart
Normal 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;
|
||||||
|
}
|
||||||
35
lib/core/api/v1/public_accounts.dart
Normal file
35
lib/core/api/v1/public_accounts.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
lib/core/api/v1/refresh_session.dart
Normal file
28
lib/core/api/v1/refresh_session.dart
Normal 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
22
lib/core/api/v1/test.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
71
lib/core/grpc/grpc_auth_interceptor.dart
Normal file
71
lib/core/grpc/grpc_auth_interceptor.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
lib/core/grpc/grpc_channel_provider.dart
Normal file
16
lib/core/grpc/grpc_channel_provider.dart
Normal 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;
|
||||||
|
});
|
||||||
54
lib/core/grpc/grpc_client.dart
Normal file
54
lib/core/grpc/grpc_client.dart
Normal 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);
|
||||||
|
});
|
||||||
30
lib/core/router/app_router.dart
Normal file
30
lib/core/router/app_router.dart
Normal 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(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
167
lib/core/tokens/application/tokens_application.dart
Normal file
167
lib/core/tokens/application/tokens_application.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
lib/core/tokens/data/token_storage_repository.dart
Normal file
42
lib/core/tokens/data/token_storage_repository.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
12
lib/features/authorization/application/sign_in_data.dart
Normal file
12
lib/features/authorization/application/sign_in_data.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
lib/features/authorization/application/sign_up_data.dart
Normal file
23
lib/features/authorization/application/sign_up_data.dart
Normal 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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
68
lib/features/authorization/presentation/.conform.8331486.decoration.dart
Executable file
68
lib/features/authorization/presentation/.conform.8331486.decoration.dart
Executable 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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
89
lib/features/authorization/presentation/decoration.dart
Normal file
89
lib/features/authorization/presentation/decoration.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
144
lib/features/authorization/presentation/login_form.dart
Normal file
144
lib/features/authorization/presentation/login_form.dart
Normal 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(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
105
lib/features/authorization/presentation/register_form.dart
Normal file
105
lib/features/authorization/presentation/register_form.dart
Normal 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'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
31
lib/features/dashboard/presentation/dashboard_page.dart
Normal file
31
lib/features/dashboard/presentation/dashboard_page.dart
Normal 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"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
lib/features/test/application/test_controller.dart
Normal file
28
lib/features/test/application/test_controller.dart
Normal 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';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
30
lib/features/test/presentation/ping_page.dart
Normal file
30
lib/features/test/presentation/ping_page.dart
Normal 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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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:flutter/material.dart';
|
||||||
import 'package:grpc/grpc_web.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
import 'package:flutter/semantics.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 {
|
void main() async {
|
||||||
await dotenv.load(fileName: ".env");
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
String backendURL = dotenv.env['SOFTPLAYER_BACKEND_URL']!;
|
|
||||||
GrpcWebClientChannel grpcChannel = GrpcWebClientChannel.xhr(
|
|
||||||
Uri.parse(backendURL),
|
|
||||||
);
|
|
||||||
|
|
||||||
runApp(MyApp(channel: grpcChannel));
|
await dotenv.load(fileName: '.env');
|
||||||
SemanticsBinding.instance.ensureSemantics();
|
runApp(const ProviderScope(child: App()));
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class App extends ConsumerWidget {
|
||||||
const MyApp({super.key, required this.channel});
|
const App({super.key});
|
||||||
final GrpcWebClientChannel channel;
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return MaterialApp(
|
final router = ref.watch(goRouterProvider);
|
||||||
debugShowCheckedModeBanner: true,
|
return MaterialApp.router(
|
||||||
title: 'Softplayer',
|
routerConfig: router,
|
||||||
home: RootWidget(channel: channel),
|
theme: AppTheme.light(),
|
||||||
);
|
darkTheme: AppTheme.dark(),
|
||||||
}
|
themeMode: ThemeMode.light,
|
||||||
}
|
|
||||||
|
|
||||||
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"),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
lib/shared/ui/colors/dark_mode.dart
Normal file
8
lib/shared/ui/colors/dark_mode.dart
Normal 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);
|
||||||
|
}
|
||||||
21
lib/shared/ui/colors/light_mode.dart
Normal file
21
lib/shared/ui/colors/light_mode.dart
Normal 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);
|
||||||
|
}
|
||||||
109
lib/shared/ui/theme/app_theme.dart
Normal file
109
lib/shared/ui/theme/app_theme.dart
Normal 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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
477
pubspec.lock
477
pubspec.lock
@@ -1,6 +1,30 @@
|
|||||||
# Generated by pub
|
# Generated by pub
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
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:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -25,6 +49,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
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:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -33,6 +65,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
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:
|
collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -41,6 +81,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.19.1"
|
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:
|
crypto:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -81,6 +137,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.3"
|
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:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -110,11 +190,96 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.0"
|
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:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
google_cloud:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -147,6 +312,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.1.0"
|
version: "5.1.0"
|
||||||
|
hooks:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: hooks
|
||||||
|
sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.3"
|
||||||
http:
|
http:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -163,6 +336,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.1"
|
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:
|
http_parser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -179,6 +360,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.5"
|
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:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -211,6 +416,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.0"
|
version: "6.1.0"
|
||||||
|
logging:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: logging
|
||||||
|
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.0"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -231,10 +444,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.0"
|
version: "1.18.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -243,6 +456,38 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
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:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -251,14 +496,110 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.1"
|
version: "1.9.1"
|
||||||
protobuf:
|
path_provider:
|
||||||
dependency: transitive
|
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:
|
description:
|
||||||
name: protobuf
|
name: protobuf
|
||||||
sha256: "75ec242d22e950bdcc79ee38dd520ce4ee0bc491d7fadc4ea47694604d22bf06"
|
sha256: "75ec242d22e950bdcc79ee38dd520ce4ee0bc491d7fadc4ea47694604d22bf06"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.0"
|
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:
|
shelf:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -267,6 +608,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.2"
|
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:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -276,11 +641,27 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: a9a7f05332596f776b37d2757b33f06f6faa92e9
|
ref: e801928517cee5298eb289f0993eec52e2186d86
|
||||||
resolved-ref: a9a7f05332596f776b37d2757b33f06f6faa92e9
|
resolved-ref: e801928517cee5298eb289f0993eec52e2186d86
|
||||||
url: "https://gitea.badhouseplants.net/softplayer/softplayer-dart-proto.git"
|
url: "https://gitea.badhouseplants.net/softplayer/softplayer-dart-proto.git"
|
||||||
source: git
|
source: git
|
||||||
version: "1.0.0"
|
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:
|
source_span:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -297,6 +678,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.12.1"
|
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:
|
stream_channel:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -321,14 +710,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.2"
|
version: "1.2.2"
|
||||||
|
test:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: test
|
||||||
|
sha256: "8d9ceddbab833f180fbefed08afa76d7c03513dfdba87ffcec2718b02bbcbf20"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.31.0"
|
||||||
test_api:
|
test_api:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
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:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -353,6 +758,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "15.2.0"
|
version: "15.2.0"
|
||||||
|
watcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: watcher
|
||||||
|
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.1"
|
||||||
web:
|
web:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -361,6 +774,54 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
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:
|
sdks:
|
||||||
dart: ">=3.11.5 <4.0.0"
|
dart: ">=3.11.5 <4.0.0"
|
||||||
flutter: ">=3.18.0-18.0.pre.54"
|
flutter: ">=3.38.4"
|
||||||
|
|||||||
14
pubspec.yaml
14
pubspec.yaml
@@ -11,13 +11,18 @@ dependencies:
|
|||||||
softplayer_dart_proto:
|
softplayer_dart_proto:
|
||||||
git:
|
git:
|
||||||
url: https://gitea.badhouseplants.net/softplayer/softplayer-dart-proto.git
|
url: https://gitea.badhouseplants.net/softplayer/softplayer-dart-proto.git
|
||||||
ref: a9a7f05332596f776b37d2757b33f06f6faa92e9
|
ref: e801928517cee5298eb289f0993eec52e2186d86
|
||||||
cupertino_icons: ^1.0.9
|
cupertino_icons: ^1.0.9
|
||||||
grpc: 5.1.0
|
grpc: 5.1.0
|
||||||
http: ^1.6.0
|
http: ^1.6.0
|
||||||
dio: ^5.9.2
|
dio: ^5.9.2
|
||||||
flutter_dotenv: ^6.0.1
|
flutter_dotenv: ^6.0.1
|
||||||
web: ^1.1.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:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
@@ -27,3 +32,10 @@ flutter:
|
|||||||
assets:
|
assets:
|
||||||
- assets/
|
- assets/
|
||||||
- .env
|
- .env
|
||||||
|
fonts:
|
||||||
|
- family: Syne
|
||||||
|
fonts:
|
||||||
|
- asset: assets/fonts/syne/Syne-Regular.ttf
|
||||||
|
- family: Inter
|
||||||
|
fonts:
|
||||||
|
- asset: assets/fonts/inter/Inter-Regular.ttf
|
||||||
|
|||||||
Reference in New Issue
Block a user