full stack first commit
This commit is contained in:
25
b0esche_cloud/lib/blocs/activity/activity_bloc.dart
Normal file
25
b0esche_cloud/lib/blocs/activity/activity_bloc.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'activity_event.dart';
|
||||
import 'activity_state.dart';
|
||||
import '../../services/activity_api.dart';
|
||||
|
||||
class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
|
||||
final ActivityApi activityApi;
|
||||
|
||||
ActivityBloc(this.activityApi) : super(ActivityInitial()) {
|
||||
on<LoadOrgActivities>(_onLoadOrgActivities);
|
||||
}
|
||||
|
||||
void _onLoadOrgActivities(
|
||||
LoadOrgActivities event,
|
||||
Emitter<ActivityState> emit,
|
||||
) async {
|
||||
emit(ActivityLoading());
|
||||
try {
|
||||
final activities = await activityApi.getOrgActivities(event.orgId);
|
||||
emit(ActivityLoaded(activities: activities));
|
||||
} catch (e) {
|
||||
emit(ActivityError(e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
17
b0esche_cloud/lib/blocs/activity/activity_event.dart
Normal file
17
b0esche_cloud/lib/blocs/activity/activity_event.dart
Normal file
@@ -0,0 +1,17 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
abstract class ActivityEvent extends Equatable {
|
||||
const ActivityEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class LoadOrgActivities extends ActivityEvent {
|
||||
final String orgId;
|
||||
|
||||
const LoadOrgActivities(this.orgId);
|
||||
|
||||
@override
|
||||
List<Object> get props => [orgId];
|
||||
}
|
||||
31
b0esche_cloud/lib/blocs/activity/activity_state.dart
Normal file
31
b0esche_cloud/lib/blocs/activity/activity_state.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../organization/organization_state.dart';
|
||||
|
||||
abstract class ActivityState extends Equatable {
|
||||
const ActivityState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class ActivityInitial extends ActivityState {}
|
||||
|
||||
class ActivityLoading extends ActivityState {}
|
||||
|
||||
class ActivityLoaded extends ActivityState {
|
||||
final List<Activity> activities;
|
||||
|
||||
const ActivityLoaded({required this.activities});
|
||||
|
||||
@override
|
||||
List<Object> get props => [activities];
|
||||
}
|
||||
|
||||
class ActivityError extends ActivityState {
|
||||
final String error;
|
||||
|
||||
const ActivityError(this.error);
|
||||
|
||||
@override
|
||||
List<Object> get props => [error];
|
||||
}
|
||||
@@ -11,14 +11,10 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
|
||||
void _onLoginRequested(LoginRequested event, Emitter<AuthState> emit) async {
|
||||
emit(AuthLoading());
|
||||
// Simulate API call to go.b0esche.cloud/auth/login
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
if (event.email.isNotEmpty && event.password.isNotEmpty) {
|
||||
// Assume JWT received
|
||||
emit(AuthAuthenticated(token: 'fake-jwt', userId: 'user123'));
|
||||
} else {
|
||||
emit(const AuthFailure('Invalid credentials'));
|
||||
}
|
||||
// Redirect to Go auth/login
|
||||
// For web, use window.location or url_launcher
|
||||
// Assume handled in UI
|
||||
emit(const AuthFailure('Redirect to login URL'));
|
||||
}
|
||||
|
||||
void _onLogoutRequested(
|
||||
|
||||
@@ -7,15 +7,7 @@ abstract class AuthEvent extends Equatable {
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class LoginRequested extends AuthEvent {
|
||||
final String email;
|
||||
final String password;
|
||||
|
||||
const LoginRequested(this.email, this.password);
|
||||
|
||||
@override
|
||||
List<Object> get props => [email, password];
|
||||
}
|
||||
class LoginRequested extends AuthEvent {}
|
||||
|
||||
class LogoutRequested extends AuthEvent {}
|
||||
|
||||
|
||||
@@ -8,14 +8,20 @@ import '../file_browser/file_browser_bloc.dart';
|
||||
import '../file_browser/file_browser_event.dart';
|
||||
import '../upload/upload_bloc.dart';
|
||||
import '../upload/upload_event.dart';
|
||||
import '../../services/org_api.dart';
|
||||
|
||||
class OrganizationBloc extends Bloc<OrganizationEvent, OrganizationState> {
|
||||
final PermissionBloc permissionBloc;
|
||||
final FileBrowserBloc fileBrowserBloc;
|
||||
final UploadBloc uploadBloc;
|
||||
final OrgApi orgApi;
|
||||
|
||||
OrganizationBloc(this.permissionBloc, this.fileBrowserBloc, this.uploadBloc)
|
||||
: super(OrganizationInitial()) {
|
||||
OrganizationBloc(
|
||||
this.permissionBloc,
|
||||
this.fileBrowserBloc,
|
||||
this.uploadBloc,
|
||||
this.orgApi,
|
||||
) : super(OrganizationInitial()) {
|
||||
on<LoadOrganizations>(_onLoadOrganizations);
|
||||
on<SelectOrganization>(_onSelectOrganization);
|
||||
on<CreateOrganization>(_onCreateOrganization);
|
||||
@@ -27,14 +33,13 @@ class OrganizationBloc extends Bloc<OrganizationEvent, OrganizationState> {
|
||||
) async {
|
||||
emit(OrganizationLoading());
|
||||
try {
|
||||
// Simulate loading orgs from API
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
final orgs = [
|
||||
const Organization(id: 'org1', name: 'Personal', role: 'admin'),
|
||||
const Organization(id: 'org2', name: 'Company Inc', role: 'edit'),
|
||||
const Organization(id: 'org3', name: 'Side Project', role: 'admin'),
|
||||
];
|
||||
emit(OrganizationLoaded(organizations: orgs, selectedOrg: orgs.first));
|
||||
final orgs = await orgApi.getOrganizations();
|
||||
emit(
|
||||
OrganizationLoaded(
|
||||
organizations: orgs,
|
||||
selectedOrg: orgs.isNotEmpty ? orgs.first : null,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(OrganizationError(e.toString()));
|
||||
}
|
||||
@@ -103,13 +108,7 @@ class OrganizationBloc extends Bloc<OrganizationEvent, OrganizationState> {
|
||||
),
|
||||
);
|
||||
try {
|
||||
// Simulate API call
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
final newOrg = Organization(
|
||||
id: 'org${currentState.organizations.length + 1}',
|
||||
name: name,
|
||||
role: 'admin',
|
||||
);
|
||||
final newOrg = await orgApi.createOrganization(name);
|
||||
final updatedOrgs = [...currentState.organizations, newOrg];
|
||||
emit(
|
||||
OrganizationLoaded(organizations: updatedOrgs, selectedOrg: newOrg),
|
||||
|
||||
@@ -3,20 +3,70 @@ import 'package:equatable/equatable.dart';
|
||||
class Organization extends Equatable {
|
||||
final String id;
|
||||
final String name;
|
||||
final String role; // view, edit, admin
|
||||
final String? shortCode;
|
||||
final int? color;
|
||||
final String slug;
|
||||
final DateTime createdAt;
|
||||
|
||||
const Organization({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.role,
|
||||
this.shortCode,
|
||||
this.color,
|
||||
required this.slug,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, name, role, shortCode, color];
|
||||
List<Object?> get props => [id, name, slug, createdAt];
|
||||
|
||||
factory Organization.fromJson(Map<String, dynamic> json) {
|
||||
return Organization(
|
||||
id: json['id'],
|
||||
name: json['name'],
|
||||
slug: json['slug'],
|
||||
createdAt: DateTime.parse(json['createdAt']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Activity extends Equatable {
|
||||
final String id;
|
||||
final String userId;
|
||||
final String orgId;
|
||||
final String? fileId;
|
||||
final String action;
|
||||
final Map<String, dynamic> metadata;
|
||||
final DateTime timestamp;
|
||||
|
||||
const Activity({
|
||||
required this.id,
|
||||
required this.userId,
|
||||
required this.orgId,
|
||||
this.fileId,
|
||||
required this.action,
|
||||
required this.metadata,
|
||||
required this.timestamp,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
userId,
|
||||
orgId,
|
||||
fileId,
|
||||
action,
|
||||
metadata,
|
||||
timestamp,
|
||||
];
|
||||
|
||||
factory Activity.fromJson(Map<String, dynamic> json) {
|
||||
return Activity(
|
||||
id: json['id'],
|
||||
userId: json['userId'],
|
||||
orgId: json['orgId'],
|
||||
fileId: json['fileId'],
|
||||
action: json['action'],
|
||||
metadata: json['metadata'] ?? {},
|
||||
timestamp: DateTime.parse(json['timestamp']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class OrganizationState extends Equatable {
|
||||
|
||||
@@ -15,8 +15,8 @@ class SessionBloc extends Bloc<SessionEvent, SessionState> {
|
||||
|
||||
void _onSessionStarted(SessionStarted event, Emitter<SessionState> emit) {
|
||||
final expiresAt = DateTime.now().add(
|
||||
const Duration(hours: 1),
|
||||
); // Fake expiry
|
||||
const Duration(minutes: 15),
|
||||
); // Match Go
|
||||
emit(SessionActive(token: event.token, expiresAt: expiresAt));
|
||||
_startExpiryTimer(expiresAt);
|
||||
}
|
||||
@@ -27,7 +27,7 @@ class SessionBloc extends Bloc<SessionEvent, SessionState> {
|
||||
}
|
||||
|
||||
void _onSessionRefreshed(SessionRefreshed event, Emitter<SessionState> emit) {
|
||||
final expiresAt = DateTime.now().add(const Duration(hours: 1));
|
||||
final expiresAt = DateTime.now().add(const Duration(minutes: 15));
|
||||
emit(SessionActive(token: event.newToken, expiresAt: expiresAt));
|
||||
_startExpiryTimer(expiresAt);
|
||||
}
|
||||
|
||||
@@ -7,8 +7,12 @@ import 'blocs/organization/organization_bloc.dart';
|
||||
import 'blocs/permission/permission_bloc.dart';
|
||||
import 'blocs/file_browser/file_browser_bloc.dart';
|
||||
import 'blocs/upload/upload_bloc.dart';
|
||||
import 'blocs/activity/activity_bloc.dart';
|
||||
import 'repositories/mock_file_repository.dart';
|
||||
import 'services/file_service.dart';
|
||||
import 'services/api_client.dart';
|
||||
import 'services/org_api.dart';
|
||||
import 'services/activity_api.dart';
|
||||
import 'pages/home_page.dart';
|
||||
import 'pages/login_form.dart';
|
||||
import 'pages/file_explorer.dart';
|
||||
@@ -58,10 +62,13 @@ class MainApp extends StatelessWidget {
|
||||
BlocProvider<SessionBloc>(create: (_) => SessionBloc()),
|
||||
BlocProvider<PermissionBloc>(create: (_) => PermissionBloc()),
|
||||
BlocProvider<FileBrowserBloc>(
|
||||
create: (_) => FileBrowserBloc(FileService(MockFileRepository())),
|
||||
create: (context) => FileBrowserBloc(
|
||||
FileService(ApiClient(context.read<SessionBloc>())),
|
||||
),
|
||||
),
|
||||
BlocProvider<UploadBloc>(
|
||||
create: (_) => UploadBloc(MockFileRepository()),
|
||||
create: (_) =>
|
||||
UploadBloc(MockFileRepository()), // keep mock for upload
|
||||
),
|
||||
BlocProvider<OrganizationBloc>(
|
||||
lazy: true,
|
||||
@@ -69,8 +76,13 @@ class MainApp extends StatelessWidget {
|
||||
context.read<PermissionBloc>(),
|
||||
context.read<FileBrowserBloc>(),
|
||||
context.read<UploadBloc>(),
|
||||
OrgApi(ApiClient(context.read<SessionBloc>())),
|
||||
),
|
||||
),
|
||||
BlocProvider<ActivityBloc>(
|
||||
create: (context) =>
|
||||
ActivityBloc(ActivityApi(ApiClient(context.read<SessionBloc>()))),
|
||||
),
|
||||
],
|
||||
child: MaterialApp.router(
|
||||
routerConfig: _router,
|
||||
|
||||
@@ -110,8 +110,8 @@ class _LoginFormState extends State<LoginForm> {
|
||||
onPressed: () {
|
||||
context.read<AuthBloc>().add(
|
||||
LoginRequested(
|
||||
_emailController.text,
|
||||
_passwordController.text,
|
||||
// _emailController.text,
|
||||
// _passwordController.text,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
15
b0esche_cloud/lib/services/activity_api.dart
Normal file
15
b0esche_cloud/lib/services/activity_api.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import '../blocs/organization/organization_state.dart';
|
||||
import 'api_client.dart';
|
||||
|
||||
class ActivityApi {
|
||||
final ApiClient _apiClient;
|
||||
|
||||
ActivityApi(this._apiClient);
|
||||
|
||||
Future<List<Activity>> getOrgActivities(String orgId) async {
|
||||
return await _apiClient.getList(
|
||||
'/orgs/$orgId/activity',
|
||||
fromJson: (data) => Activity.fromJson(data),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -102,6 +102,19 @@ class ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<T>> getList<T>(
|
||||
String path, {
|
||||
Map<String, dynamic>? queryParameters,
|
||||
required T Function(dynamic data) fromJson,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.get(path, queryParameters: queryParameters);
|
||||
return (response.data as List).map(fromJson).toList();
|
||||
} on DioException catch (e) {
|
||||
throw _handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
ApiError _handleError(DioException e) {
|
||||
final status = e.response?.statusCode;
|
||||
final data = e.response?.data;
|
||||
|
||||
@@ -3,38 +3,39 @@ import '../models/viewer_session.dart';
|
||||
import '../models/editor_session.dart';
|
||||
import '../models/annotation.dart';
|
||||
import '../repositories/file_repository.dart';
|
||||
import 'api_client.dart';
|
||||
|
||||
class FileService {
|
||||
final FileRepository _fileRepository;
|
||||
final ApiClient _apiClient;
|
||||
|
||||
FileService(this._fileRepository);
|
||||
FileService(this._apiClient);
|
||||
|
||||
Future<List<FileItem>> getFiles(String orgId, String path) async {
|
||||
if (path.isEmpty) {
|
||||
throw Exception('Path cannot be empty');
|
||||
}
|
||||
return await _fileRepository.getFiles(orgId, path);
|
||||
return await _apiClient.getList(
|
||||
'/orgs/$orgId/files',
|
||||
fromJson: (data) => FileItem(
|
||||
name: data['name'],
|
||||
path: data['path'],
|
||||
type: data['type'] == 'file' ? FileType.file : FileType.folder,
|
||||
size: data['size'],
|
||||
lastModified: DateTime.parse(data['lastModified']),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<FileItem?> getFile(String orgId, String path) async {
|
||||
if (path.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return await _fileRepository.getFile(orgId, path);
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
Future<void> uploadFile(String orgId, FileItem file) async {
|
||||
if (file.name.isEmpty) {
|
||||
throw Exception('File name cannot be empty');
|
||||
}
|
||||
await _fileRepository.uploadFile(orgId, file);
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
Future<void> deleteFile(String orgId, String path) async {
|
||||
if (path.isEmpty) {
|
||||
throw Exception('Path cannot be empty');
|
||||
}
|
||||
await _fileRepository.deleteFile(orgId, path);
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
Future<void> createFolder(
|
||||
@@ -42,10 +43,7 @@ class FileService {
|
||||
String parentPath,
|
||||
String folderName,
|
||||
) async {
|
||||
if (folderName.isEmpty) {
|
||||
throw Exception('Folder name cannot be empty');
|
||||
}
|
||||
await _fileRepository.createFolder(orgId, parentPath, folderName);
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
Future<void> moveFile(
|
||||
@@ -53,24 +51,15 @@ class FileService {
|
||||
String sourcePath,
|
||||
String targetPath,
|
||||
) async {
|
||||
if (sourcePath.isEmpty || targetPath.isEmpty) {
|
||||
throw Exception('Paths cannot be empty');
|
||||
}
|
||||
await _fileRepository.moveFile(orgId, sourcePath, targetPath);
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
Future<void> renameFile(String orgId, String path, String newName) async {
|
||||
if (path.isEmpty || newName.isEmpty) {
|
||||
throw Exception('Path and new name cannot be empty');
|
||||
}
|
||||
await _fileRepository.renameFile(orgId, path, newName);
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
Future<List<FileItem>> searchFiles(String orgId, String query) async {
|
||||
if (query.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
return await _fileRepository.searchFiles(orgId, query);
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
Future<ViewerSession> requestViewerSession(
|
||||
@@ -80,7 +69,10 @@ class FileService {
|
||||
if (orgId.isEmpty || fileId.isEmpty) {
|
||||
throw Exception('OrgId and fileId cannot be empty');
|
||||
}
|
||||
return await _fileRepository.requestViewerSession(orgId, fileId);
|
||||
return await _apiClient.get(
|
||||
'/orgs/$orgId/files/$fileId/view',
|
||||
fromJson: (data) => ViewerSession.fromJson(data),
|
||||
);
|
||||
}
|
||||
|
||||
Future<EditorSession> requestEditorSession(
|
||||
@@ -90,7 +82,10 @@ class FileService {
|
||||
if (orgId.isEmpty || fileId.isEmpty) {
|
||||
throw Exception('OrgId and fileId cannot be empty');
|
||||
}
|
||||
return await _fileRepository.requestEditorSession(orgId, fileId);
|
||||
return await _apiClient.get(
|
||||
'/orgs/$orgId/files/$fileId/edit',
|
||||
fromJson: (data) => EditorSession.fromJson(data),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> saveAnnotations(
|
||||
@@ -101,6 +96,13 @@ class FileService {
|
||||
if (orgId.isEmpty || fileId.isEmpty) {
|
||||
throw Exception('OrgId and fileId cannot be empty');
|
||||
}
|
||||
await _fileRepository.saveAnnotations(orgId, fileId, annotations);
|
||||
await _apiClient.post(
|
||||
'/orgs/$orgId/files/$fileId/annotations',
|
||||
data: {
|
||||
'annotations': annotations.map((a) => a.toJson()).toList(),
|
||||
'baseVersionId': '1', // mock
|
||||
},
|
||||
fromJson: (data) => null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
23
b0esche_cloud/lib/services/org_api.dart
Normal file
23
b0esche_cloud/lib/services/org_api.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
import '../blocs/organization/organization_state.dart';
|
||||
import 'api_client.dart';
|
||||
|
||||
class OrgApi {
|
||||
final ApiClient _apiClient;
|
||||
|
||||
OrgApi(this._apiClient);
|
||||
|
||||
Future<List<Organization>> getOrganizations() async {
|
||||
return await _apiClient.getList(
|
||||
'/orgs',
|
||||
fromJson: (data) => Organization.fromJson(data),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Organization> createOrganization(String name) async {
|
||||
return await _apiClient.post(
|
||||
'/orgs',
|
||||
data: {'name': name},
|
||||
fromJson: (data) => Organization.fromJson(data),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user