b0-cloud first commit

This commit is contained in:
Leon Bösche
2025-12-16 18:09:20 +01:00
parent fcac7ac69f
commit 7d372a1aac
60 changed files with 3760 additions and 10 deletions

Binary file not shown.

View File

@@ -0,0 +1,2 @@
license: Public Domain
link: https://www.fontspace.com/pixelated-elegance-font-f126145

View File

@@ -0,0 +1,2 @@
license: Freeware
link: https://www.fontspace.com/sparky-stones-font-f88004

View File

@@ -0,0 +1,5 @@
This font is free to use personal and commercial works too. But you can't sell them direktly.
Please don't make illegal copies of the fonts.
Thanks and regards: Koczman B<>lint

View File

@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

View File

@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

43
b0esche_cloud/ios/Podfile Normal file
View File

@@ -0,0 +1,43 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '13.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end

View File

@@ -0,0 +1,42 @@
import 'package:bloc/bloc.dart';
import 'auth_event.dart';
import 'auth_state.dart';
class AuthBloc extends Bloc<AuthEvent, AuthState> {
AuthBloc() : super(AuthInitial()) {
on<LoginRequested>(_onLoginRequested);
on<LogoutRequested>(_onLogoutRequested);
on<CheckAuthRequested>(_onCheckAuthRequested);
}
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'));
}
}
void _onLogoutRequested(
LogoutRequested event,
Emitter<AuthState> emit,
) async {
emit(AuthLoading());
// Call logout API
await Future.delayed(const Duration(milliseconds: 500));
emit(AuthUnauthenticated());
}
void _onCheckAuthRequested(
CheckAuthRequested event,
Emitter<AuthState> emit,
) async {
// Check if token is valid
// For now, assume unauthenticated
emit(AuthUnauthenticated());
}
}

View File

@@ -0,0 +1,22 @@
import 'package:equatable/equatable.dart';
abstract class AuthEvent extends Equatable {
const AuthEvent();
@override
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 LogoutRequested extends AuthEvent {}
class CheckAuthRequested extends AuthEvent {}

View File

@@ -0,0 +1,33 @@
import 'package:equatable/equatable.dart';
abstract class AuthState extends Equatable {
const AuthState();
@override
List<Object> get props => [];
}
class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}
class AuthAuthenticated extends AuthState {
final String token;
final String userId;
const AuthAuthenticated({required this.token, required this.userId});
@override
List<Object> get props => [token, userId];
}
class AuthUnauthenticated extends AuthState {}
class AuthFailure extends AuthState {
final String error;
const AuthFailure(this.error);
@override
List<Object> get props => [error];
}

View File

@@ -0,0 +1,48 @@
import 'package:bloc/bloc.dart';
import 'document_viewer_event.dart';
import 'document_viewer_state.dart';
class DocumentViewerBloc
extends Bloc<DocumentViewerEvent, DocumentViewerState> {
DocumentViewerBloc() : super(ViewerInitial()) {
on<OpenDocument>(_onOpenDocument);
on<CloseDocument>(_onCloseDocument);
on<ReloadDocument>(_onReloadDocument);
}
void _onOpenDocument(
OpenDocument event,
Emitter<DocumentViewerState> emit,
) async {
emit(ViewerLoading());
// Simulate requesting viewer session from backend
await Future.delayed(const Duration(seconds: 2));
// Mock: assume PDF viewer
final viewUrl = 'https://storage.b0esche.cloud/view/${event.fileId}';
final canEdit = false; // Based on permissions
final fileName = 'document.pdf';
emit(ViewerReady(viewUrl: viewUrl, canEdit: canEdit, fileName: fileName));
}
void _onCloseDocument(
CloseDocument event,
Emitter<DocumentViewerState> emit,
) {
emit(ViewerInitial());
}
void _onReloadDocument(
ReloadDocument event,
Emitter<DocumentViewerState> emit,
) {
// Reload current document
final currentState = state;
if (currentState is ViewerReady) {
emit(ViewerLoading());
// Simulate reload
Future.delayed(const Duration(seconds: 1), () {
emit(currentState);
});
}
}
}

View File

@@ -0,0 +1,22 @@
import 'package:equatable/equatable.dart';
abstract class DocumentViewerEvent extends Equatable {
const DocumentViewerEvent();
@override
List<Object> get props => [];
}
class OpenDocument extends DocumentViewerEvent {
final String fileId;
final String orgId;
const OpenDocument({required this.fileId, required this.orgId});
@override
List<Object> get props => [fileId, orgId];
}
class CloseDocument extends DocumentViewerEvent {}
class ReloadDocument extends DocumentViewerEvent {}

View File

@@ -0,0 +1,36 @@
import 'package:equatable/equatable.dart';
abstract class DocumentViewerState extends Equatable {
const DocumentViewerState();
@override
List<Object> get props => [];
}
class ViewerInitial extends DocumentViewerState {}
class ViewerLoading extends DocumentViewerState {}
class ViewerReady extends DocumentViewerState {
final String viewUrl;
final bool canEdit;
final String fileName;
const ViewerReady({
required this.viewUrl,
required this.canEdit,
required this.fileName,
});
@override
List<Object> get props => [viewUrl, canEdit, fileName];
}
class ViewerError extends DocumentViewerState {
final String error;
const ViewerError(this.error);
@override
List<Object> get props => [error];
}

View File

@@ -0,0 +1,85 @@
import 'package:bloc/bloc.dart';
import 'file_browser_event.dart';
import 'file_browser_state.dart';
import '../../repositories/file_repository.dart';
class FileBrowserBloc extends Bloc<FileBrowserEvent, FileBrowserState> {
final FileRepository _fileRepository;
String _currentOrgId = '';
String _currentPath = '/';
FileBrowserBloc(this._fileRepository) : super(DirectoryInitial()) {
on<LoadDirectory>(_onLoadDirectory);
on<NavigateToFolder>(_onNavigateToFolder);
on<RefreshDirectory>(_onRefreshDirectory);
on<ApplySort>(_onApplySort);
on<ApplyFilter>(_onApplyFilter);
on<ResetFileBrowser>(_onResetFileBrowser);
}
void _onLoadDirectory(
LoadDirectory event,
Emitter<FileBrowserState> emit,
) async {
emit(DirectoryLoading());
_currentOrgId = event.orgId;
_currentPath = event.path;
try {
final files = await _fileRepository.getFiles(event.orgId, event.path);
final breadcrumbs = _generateBreadcrumbs(event.path);
if (files.isEmpty) {
emit(DirectoryEmpty());
} else {
emit(DirectoryLoaded(files: files, breadcrumbs: breadcrumbs));
}
} catch (e) {
emit(DirectoryError(e.toString()));
}
}
void _onNavigateToFolder(
NavigateToFolder event,
Emitter<FileBrowserState> emit,
) async {
// Assume event.folderId is the path
add(LoadDirectory(orgId: _currentOrgId, path: event.folderId));
}
void _onRefreshDirectory(
RefreshDirectory event,
Emitter<FileBrowserState> emit,
) async {
add(LoadDirectory(orgId: _currentOrgId, path: _currentPath));
}
void _onApplySort(ApplySort event, Emitter<FileBrowserState> emit) {
// Implement sorting
// For now, just refresh
add(RefreshDirectory());
}
void _onApplyFilter(ApplyFilter event, Emitter<FileBrowserState> emit) {
// Implement filtering
add(RefreshDirectory());
}
void _onResetFileBrowser(
ResetFileBrowser event,
Emitter<FileBrowserState> emit,
) {
emit(DirectoryInitial());
_currentOrgId = '';
_currentPath = '/';
}
List<Breadcrumb> _generateBreadcrumbs(String path) {
final parts = path.split('/').where((p) => p.isNotEmpty).toList();
final breadcrumbs = <Breadcrumb>[];
String currentPath = '';
for (final part in parts) {
currentPath += '/$part';
breadcrumbs.add(Breadcrumb(name: part, path: currentPath));
}
return breadcrumbs;
}
}

View File

@@ -0,0 +1,49 @@
import 'package:equatable/equatable.dart';
abstract class FileBrowserEvent extends Equatable {
const FileBrowserEvent();
@override
List<Object> get props => [];
}
class LoadDirectory extends FileBrowserEvent {
final String orgId;
final String path;
const LoadDirectory({required this.orgId, required this.path});
@override
List<Object> get props => [orgId, path];
}
class NavigateToFolder extends FileBrowserEvent {
final String folderId;
const NavigateToFolder(this.folderId);
@override
List<Object> get props => [folderId];
}
class RefreshDirectory extends FileBrowserEvent {}
class ApplySort extends FileBrowserEvent {
final String sortBy; // name, date, size
const ApplySort(this.sortBy);
@override
List<Object> get props => [sortBy];
}
class ApplyFilter extends FileBrowserEvent {
final String filter;
const ApplyFilter(this.filter);
@override
List<Object> get props => [filter];
}
class ResetFileBrowser extends FileBrowserEvent {}

View File

@@ -0,0 +1,44 @@
import 'package:equatable/equatable.dart';
import '../../models/file_item.dart';
class Breadcrumb extends Equatable {
final String name;
final String path;
const Breadcrumb({required this.name, required this.path});
@override
List<Object> get props => [name, path];
}
abstract class FileBrowserState extends Equatable {
const FileBrowserState();
@override
List<Object> get props => [];
}
class DirectoryInitial extends FileBrowserState {}
class DirectoryLoading extends FileBrowserState {}
class DirectoryLoaded extends FileBrowserState {
final List<FileItem> files;
final List<Breadcrumb> breadcrumbs;
const DirectoryLoaded({required this.files, required this.breadcrumbs});
@override
List<Object> get props => [files, breadcrumbs];
}
class DirectoryEmpty extends FileBrowserState {}
class DirectoryError extends FileBrowserState {
final String error;
const DirectoryError(this.error);
@override
List<Object> get props => [error];
}

View File

@@ -0,0 +1,61 @@
import 'package:bloc/bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'organization_event.dart';
import 'organization_state.dart';
import '../permission/permission_bloc.dart';
import '../permission/permission_event.dart';
import '../file_browser/file_browser_bloc.dart';
import '../file_browser/file_browser_event.dart';
import '../upload/upload_bloc.dart';
import '../upload/upload_event.dart';
class OrganizationBloc extends Bloc<OrganizationEvent, OrganizationState> {
final PermissionBloc permissionBloc;
final FileBrowserBloc fileBrowserBloc;
final UploadBloc uploadBloc;
OrganizationBloc(this.permissionBloc, this.fileBrowserBloc, this.uploadBloc)
: super(OrganizationInitial()) {
on<LoadOrganizations>(_onLoadOrganizations);
on<SelectOrganization>(_onSelectOrganization);
}
void _onLoadOrganizations(
LoadOrganizations event,
Emitter<OrganizationState> emit,
) async {
emit(OrganizationLoading());
// 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'),
];
emit(OrganizationLoaded(organizations: orgs));
}
void _onSelectOrganization(
SelectOrganization event,
Emitter<OrganizationState> emit,
) {
final currentState = state;
if (currentState is OrganizationLoaded) {
final selected = currentState.organizations.firstWhere(
(org) => org.id == event.orgId,
orElse: () => currentState.selectedOrg!,
);
emit(
OrganizationLoaded(
organizations: currentState.organizations,
selectedOrg: selected,
),
);
// Reset all dependent blocs
permissionBloc.add(PermissionsReset());
fileBrowserBloc.add(ResetFileBrowser());
uploadBloc.add(ResetUploads());
// Load permissions for the selected org
permissionBloc.add(LoadPermissions(event.orgId));
}
}
}

View File

@@ -0,0 +1,19 @@
import 'package:equatable/equatable.dart';
abstract class OrganizationEvent extends Equatable {
const OrganizationEvent();
@override
List<Object> get props => [];
}
class LoadOrganizations extends OrganizationEvent {}
class SelectOrganization extends OrganizationEvent {
final String orgId;
const SelectOrganization(this.orgId);
@override
List<Object> get props => [orgId];
}

View File

@@ -0,0 +1,46 @@
import 'package:equatable/equatable.dart';
class Organization extends Equatable {
final String id;
final String name;
final String role; // view, edit, admin
const Organization({
required this.id,
required this.name,
required this.role,
});
@override
List<Object> get props => [id, name, role];
}
abstract class OrganizationState extends Equatable {
const OrganizationState();
@override
List<Object> get props => [];
}
class OrganizationInitial extends OrganizationState {}
class OrganizationLoading extends OrganizationState {}
class OrganizationLoaded extends OrganizationState {
final List<Organization> organizations;
final Organization? selectedOrg;
const OrganizationLoaded({required this.organizations, this.selectedOrg});
@override
List<Object> get props => [organizations, selectedOrg ?? ''];
}
class OrganizationError extends OrganizationState {
final String error;
const OrganizationError(this.error);
@override
List<Object> get props => [error];
}

View File

@@ -0,0 +1,36 @@
import 'package:bloc/bloc.dart';
import 'permission_event.dart';
import 'permission_state.dart';
class PermissionBloc extends Bloc<PermissionEvent, PermissionState> {
PermissionBloc() : super(PermissionInitial()) {
on<LoadPermissions>(_onLoadPermissions);
on<PermissionsReset>(_onPermissionsReset);
}
void _onLoadPermissions(
LoadPermissions event,
Emitter<PermissionState> emit,
) async {
emit(PermissionLoading());
// Simulate loading permissions from backend for orgId
await Future.delayed(const Duration(seconds: 1));
// Mock capabilities based on orgId
final capabilities = Capabilities(
canRead: true,
canWrite: event.orgId == 'org1', // Only admin for personal
canShare: event.orgId == 'org1',
canAdmin: event.orgId == 'org1',
canAnnotate: true,
canEdit: true,
);
emit(PermissionLoaded(capabilities));
}
void _onPermissionsReset(
PermissionsReset event,
Emitter<PermissionState> emit,
) {
emit(PermissionInitial());
}
}

View File

@@ -0,0 +1,19 @@
import 'package:equatable/equatable.dart';
abstract class PermissionEvent extends Equatable {
const PermissionEvent();
@override
List<Object> get props => [];
}
class LoadPermissions extends PermissionEvent {
final String orgId;
const LoadPermissions(this.orgId);
@override
List<Object> get props => [orgId];
}
class PermissionsReset extends PermissionEvent {}

View File

@@ -0,0 +1,58 @@
import 'package:equatable/equatable.dart';
class Capabilities extends Equatable {
final bool canRead;
final bool canWrite;
final bool canShare;
final bool canAdmin;
final bool canAnnotate;
final bool canEdit;
const Capabilities({
required this.canRead,
required this.canWrite,
required this.canShare,
required this.canAdmin,
required this.canAnnotate,
required this.canEdit,
});
@override
List<Object> get props => [
canRead,
canWrite,
canShare,
canAdmin,
canAnnotate,
canEdit,
];
}
abstract class PermissionState extends Equatable {
const PermissionState();
@override
List<Object> get props => [];
}
class PermissionInitial extends PermissionState {}
class PermissionLoading extends PermissionState {}
class PermissionLoaded extends PermissionState {
final Capabilities capabilities;
const PermissionLoaded(this.capabilities);
@override
List<Object> get props => [capabilities];
}
class PermissionDenied extends PermissionState {
final String error;
const PermissionDenied(this.error);
@override
List<Object> get props => [error];
}

View File

@@ -0,0 +1,53 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'session_event.dart';
import 'session_state.dart';
class SessionBloc extends Bloc<SessionEvent, SessionState> {
Timer? _expiryTimer;
SessionBloc() : super(SessionInitial()) {
on<SessionStarted>(_onSessionStarted);
on<SessionExpired>(_onSessionExpired);
on<SessionRefreshed>(_onSessionRefreshed);
on<SessionEnded>(_onSessionEnded);
}
void _onSessionStarted(SessionStarted event, Emitter<SessionState> emit) {
final expiresAt = DateTime.now().add(
const Duration(hours: 1),
); // Fake expiry
emit(SessionActive(token: event.token, expiresAt: expiresAt));
_startExpiryTimer(expiresAt);
}
void _onSessionExpired(SessionExpired event, Emitter<SessionState> emit) {
_expiryTimer?.cancel();
emit(SessionExpiredState());
}
void _onSessionRefreshed(SessionRefreshed event, Emitter<SessionState> emit) {
final expiresAt = DateTime.now().add(const Duration(hours: 1));
emit(SessionActive(token: event.newToken, expiresAt: expiresAt));
_startExpiryTimer(expiresAt);
}
void _onSessionEnded(SessionEnded event, Emitter<SessionState> emit) {
_expiryTimer?.cancel();
emit(SessionInitial());
}
void _startExpiryTimer(DateTime expiresAt) {
_expiryTimer?.cancel();
final duration = expiresAt.difference(DateTime.now());
_expiryTimer = Timer(duration, () {
add(SessionExpired());
});
}
@override
Future<void> close() {
_expiryTimer?.cancel();
return super.close();
}
}

View File

@@ -0,0 +1,30 @@
import 'package:equatable/equatable.dart';
abstract class SessionEvent extends Equatable {
const SessionEvent();
@override
List<Object> get props => [];
}
class SessionStarted extends SessionEvent {
final String token;
const SessionStarted(this.token);
@override
List<Object> get props => [token];
}
class SessionExpired extends SessionEvent {}
class SessionRefreshed extends SessionEvent {
final String newToken;
const SessionRefreshed(this.newToken);
@override
List<Object> get props => [newToken];
}
class SessionEnded extends SessionEvent {}

View File

@@ -0,0 +1,31 @@
import 'package:equatable/equatable.dart';
abstract class SessionState extends Equatable {
const SessionState();
@override
List<Object> get props => [];
}
class SessionInitial extends SessionState {}
class SessionActive extends SessionState {
final String token;
final DateTime expiresAt;
const SessionActive({required this.token, required this.expiresAt});
@override
List<Object> get props => [token, expiresAt];
}
class SessionExpiredState extends SessionState {}
class SessionError extends SessionState {
final String error;
const SessionError(this.error);
@override
List<Object> get props => [error];
}

View File

@@ -0,0 +1,26 @@
import 'package:equatable/equatable.dart';
abstract class SharedDriveEvent extends Equatable {
const SharedDriveEvent();
@override
List<Object> get props => [];
}
class LoadSharedDrives extends SharedDriveEvent {
final String orgId;
const LoadSharedDrives(this.orgId);
@override
List<Object> get props => [orgId];
}
class OpenSharedDrive extends SharedDriveEvent {
final String driveId;
const OpenSharedDrive(this.driveId);
@override
List<Object> get props => [driveId];
}

View File

@@ -0,0 +1,120 @@
import 'package:bloc/bloc.dart';
import 'upload_event.dart';
import 'upload_state.dart';
import '../../repositories/file_repository.dart';
class UploadBloc extends Bloc<UploadEvent, UploadState> {
final FileRepository _fileRepository;
UploadBloc(this._fileRepository) : super(UploadInitial()) {
on<StartUpload>(_onStartUpload);
on<UploadProgress>(_onUploadProgress);
on<UploadPaused>(_onUploadPaused);
on<UploadCompleted>(_onUploadCompleted);
on<UploadFailed>(_onUploadFailed);
on<CancelUpload>(_onCancelUpload);
on<ResetUploads>(_onResetUploads);
}
void _onStartUpload(StartUpload event, Emitter<UploadState> emit) async {
final uploads = event.files
.map((file) => UploadItem(fileName: file.name))
.toList();
emit(UploadInProgress(uploads));
for (final file in event.files) {
try {
// Simulate upload
await _fileRepository.uploadFile(event.orgId, file);
add(UploadCompleted(file));
} catch (e) {
add(UploadFailed(fileName: file.name, error: e.toString()));
}
}
}
void _onUploadProgress(UploadProgress event, Emitter<UploadState> emit) {
final currentState = state;
if (currentState is UploadInProgress) {
final updatedUploads = currentState.uploads.map((upload) {
if (upload.fileName == event.fileName) {
return UploadItem(
fileName: upload.fileName,
progress: event.progress,
isPaused: upload.isPaused,
isCompleted: upload.isCompleted,
error: upload.error,
);
}
return upload;
}).toList();
emit(UploadInProgress(updatedUploads));
}
}
void _onUploadPaused(UploadPaused event, Emitter<UploadState> emit) {
final currentState = state;
if (currentState is UploadInProgress) {
final updatedUploads = currentState.uploads.map((upload) {
if (upload.fileName == event.fileName) {
return UploadItem(
fileName: upload.fileName,
progress: upload.progress,
isPaused: true,
isCompleted: upload.isCompleted,
error: upload.error,
);
}
return upload;
}).toList();
emit(UploadInProgress(updatedUploads));
}
}
void _onUploadCompleted(UploadCompleted event, Emitter<UploadState> emit) {
final currentState = state;
if (currentState is UploadInProgress) {
final updatedUploads = currentState.uploads.map((upload) {
if (upload.fileName == event.file.name) {
return UploadItem(
fileName: upload.fileName,
progress: 1.0,
isPaused: false,
isCompleted: true,
error: null,
);
}
return upload;
}).toList();
emit(UploadInProgress(updatedUploads));
}
}
void _onUploadFailed(UploadFailed event, Emitter<UploadState> emit) {
final currentState = state;
if (currentState is UploadInProgress) {
final updatedUploads = currentState.uploads.map((upload) {
if (upload.fileName == event.fileName) {
return UploadItem(
fileName: upload.fileName,
progress: upload.progress,
isPaused: upload.isPaused,
isCompleted: upload.isCompleted,
error: event.error,
);
}
return upload;
}).toList();
emit(UploadInProgress(updatedUploads));
}
}
void _onCancelUpload(CancelUpload event, Emitter<UploadState> emit) {
// Reset to initial
emit(UploadInitial());
}
void _onResetUploads(ResetUploads event, Emitter<UploadState> emit) {
emit(UploadInitial());
}
}

View File

@@ -0,0 +1,73 @@
import 'package:equatable/equatable.dart';
import '../../models/file_item.dart';
abstract class UploadEvent extends Equatable {
const UploadEvent();
@override
List<Object> get props => [];
}
class StartUpload extends UploadEvent {
final List<FileItem> files;
final String targetPath;
final String orgId;
const StartUpload({
required this.files,
required this.targetPath,
required this.orgId,
});
@override
List<Object> get props => [files, targetPath, orgId];
}
class UploadProgress extends UploadEvent {
final String fileName;
final double progress;
const UploadProgress({required this.fileName, required this.progress});
@override
List<Object> get props => [fileName, progress];
}
class UploadPaused extends UploadEvent {
final String fileName;
const UploadPaused(this.fileName);
@override
List<Object> get props => [fileName];
}
class UploadCompleted extends UploadEvent {
final FileItem file;
const UploadCompleted(this.file);
@override
List<Object> get props => [file];
}
class UploadFailed extends UploadEvent {
final String fileName;
final String error;
const UploadFailed({required this.fileName, required this.error});
@override
List<Object> get props => [fileName, error];
}
class CancelUpload extends UploadEvent {
final String fileName;
const CancelUpload(this.fileName);
@override
List<Object> get props => [fileName];
}
class ResetUploads extends UploadEvent {}

View File

@@ -0,0 +1,47 @@
import 'package:equatable/equatable.dart';
class UploadItem extends Equatable {
final String fileName;
final double progress;
final bool isPaused;
final bool isCompleted;
final String? error;
const UploadItem({
required this.fileName,
this.progress = 0.0,
this.isPaused = false,
this.isCompleted = false,
this.error,
});
@override
List<Object?> get props => [fileName, progress, isPaused, isCompleted, error];
}
abstract class UploadState extends Equatable {
const UploadState();
@override
List<Object> get props => [];
}
class UploadInitial extends UploadState {}
class UploadInProgress extends UploadState {
final List<UploadItem> uploads;
const UploadInProgress(this.uploads);
@override
List<Object> get props => [uploads];
}
class UploadError extends UploadState {
final String error;
const UploadError(this.error);
@override
List<Object> get props => [error];
}

View File

@@ -0,0 +1,27 @@
import 'package:get_it/get_it.dart';
import 'repositories/auth_repository.dart';
import 'repositories/file_repository.dart';
import 'repositories/mock_auth_repository.dart';
import 'repositories/mock_file_repository.dart';
import 'services/auth_service.dart';
import 'services/file_service.dart';
import 'viewmodels/login_view_model.dart';
import 'viewmodels/file_explorer_view_model.dart';
final getIt = GetIt.instance;
void configureDependencies() {
// Register repositories
getIt.registerSingleton<AuthRepository>(MockAuthRepository());
getIt.registerSingleton<FileRepository>(MockFileRepository());
// Register services
getIt.registerSingleton<AuthService>(AuthService(getIt<AuthRepository>()));
getIt.registerSingleton<FileService>(FileService(getIt<FileRepository>()));
// Register viewmodels
getIt.registerSingleton<LoginViewModel>(LoginViewModel(getIt<AuthService>()));
getIt.registerSingleton<FileExplorerViewModel>(
FileExplorerViewModel(getIt<FileService>()),
);
}

View File

@@ -1,4 +1,210 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'blocs/auth/auth_bloc.dart';
import 'blocs/session/session_bloc.dart';
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 'repositories/mock_file_repository.dart';
import 'pages/home_page.dart';
import 'pages/login_form.dart';
import 'pages/file_explorer.dart';
import 'pages/document_viewer.dart';
final GoRouter _router = GoRouter(
routes: [
GoRoute(path: '/', builder: (context, state) => const HomePage()),
GoRoute(path: '/login', builder: (context, state) => const LoginForm()),
GoRoute(
path: '/viewer/:fileId',
builder: (context, state) =>
DocumentViewer(fileId: state.pathParameters['fileId']!),
),
],
);
final ThemeData appTheme = ThemeData(
useMaterial3: true,
colorScheme: const ColorScheme(
brightness: Brightness.dark,
primary: Color(0xFF000000),
onPrimary: Color(0xFFFFFFFF),
primaryContainer: Color(0xFF1C1B1F),
onPrimaryContainer: Color(0xFFE6E1E5),
secondary: Color(0xFF5D5D5D),
onSecondary: Color(0xFFFFFFFF),
secondaryContainer: Color(0xFF2A2A2A),
onSecondaryContainer: Color(0xFFD9D9D9),
tertiary: Color(0xFF7D5260),
onTertiary: Color(0xFFFFFFFF),
tertiaryContainer: Color(0xFF633B48),
onTertiaryContainer: Color(0xFFFFD8E4),
error: Color(0xFFFFB4AB),
onError: Color(0xFF690005),
errorContainer: Color(0xFF93000A),
onErrorContainer: Color(0xFFFFDAD6),
background: Color(0xFF0F0F0F),
onBackground: Color(0xFFE6E1E5),
surface: Color(0xFF0F0F0F),
onSurface: Color(0xFFE6E1E5),
surfaceVariant: Color(0xFF49454F),
onSurfaceVariant: Color(0xFFCAC4D0),
outline: Color(0xFF938F99),
outlineVariant: Color(0xFF49454F),
shadow: Color(0xFF000000),
scrim: Color(0xFF000000),
inverseSurface: Color(0xFFE6E1E5),
onInverseSurface: Color(0xFF322F35),
inversePrimary: Color(0xFF6750A4),
surfaceTint: Color(0xFFD0BCFF),
),
textTheme: const TextTheme(
displayLarge: TextStyle(
fontSize: 57,
fontWeight: FontWeight.w400,
letterSpacing: -0.25,
color: Color(0xFFE6E1E5),
),
displayMedium: TextStyle(
fontSize: 45,
fontWeight: FontWeight.w400,
letterSpacing: 0,
color: Color(0xFFE6E1E5),
),
displaySmall: TextStyle(
fontSize: 36,
fontWeight: FontWeight.w400,
letterSpacing: 0,
color: Color(0xFFE6E1E5),
),
headlineLarge: TextStyle(
fontSize: 32,
fontWeight: FontWeight.w400,
letterSpacing: 0,
color: Color(0xFFE6E1E5),
),
headlineMedium: TextStyle(
fontSize: 28,
fontWeight: FontWeight.w400,
letterSpacing: 0,
color: Color(0xFFE6E1E5),
),
headlineSmall: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w400,
letterSpacing: 0,
color: Color(0xFFE6E1E5),
),
titleLarge: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w500,
letterSpacing: 0,
color: Color(0xFFE6E1E5),
),
titleMedium: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
letterSpacing: 0.15,
color: Color(0xFFE6E1E5),
),
titleSmall: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
letterSpacing: 0.1,
color: Color(0xFFE6E1E5),
),
bodyLarge: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
letterSpacing: 0.5,
color: Color(0xFFE6E1E5),
),
bodyMedium: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
letterSpacing: 0.25,
color: Color(0xFFE6E1E5),
),
bodySmall: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
letterSpacing: 0.4,
color: Color(0xFFE6E1E5),
),
labelLarge: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
letterSpacing: 0.1,
color: Color(0xFFE6E1E5),
),
labelMedium: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
letterSpacing: 0.5,
color: Color(0xFFE6E1E5),
),
labelSmall: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
letterSpacing: 0.5,
color: Color(0xFFE6E1E5),
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF5D5D5D),
foregroundColor: const Color(0xFFFFFFFF),
elevation: 0,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: const Color(0xFFD0BCFF),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFFE6E1E5),
side: const BorderSide(color: Color(0xFF938F99)),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
),
inputDecorationTheme: const InputDecorationTheme(
filled: true,
fillColor: Color(0xFF1C1B1F),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
borderSide: BorderSide(color: Color(0xFF938F99)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
borderSide: BorderSide(color: Color(0xFF938F99)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
borderSide: BorderSide(color: Color(0xFFD0BCFF)),
),
labelStyle: TextStyle(color: Color(0xFFCAC4D0)),
hintStyle: TextStyle(color: Color(0xFF938F99)),
),
cardTheme: CardThemeData(
color: const Color(0xFF1C1B1F),
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFF0F0F0F),
foregroundColor: Color(0xFFE6E1E5),
elevation: 0,
),
scaffoldBackgroundColor: const Color(0xFF0F0F0F),
);
void main() {
runApp(const MainApp());
@@ -9,12 +215,26 @@ class MainApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
body: Center(
child: Text('Hello World!'),
return MultiBlocProvider(
providers: [
BlocProvider<AuthBloc>(create: (_) => AuthBloc()),
BlocProvider<SessionBloc>(create: (_) => SessionBloc()),
BlocProvider<OrganizationBloc>(
create: (context) => OrganizationBloc(
context.read<PermissionBloc>(),
context.read<FileBrowserBloc>(),
context.read<UploadBloc>(),
),
),
BlocProvider<PermissionBloc>(create: (_) => PermissionBloc()),
BlocProvider<FileBrowserBloc>(
create: (_) => FileBrowserBloc(MockFileRepository()),
),
BlocProvider<UploadBloc>(
create: (_) => UploadBloc(MockFileRepository()),
),
],
child: MaterialApp.router(routerConfig: _router, theme: appTheme),
);
}
}

View File

@@ -0,0 +1,38 @@
import 'package:equatable/equatable.dart';
enum FileType { folder, file }
class FileItem extends Equatable {
final String name;
final String path;
final FileType type;
final int size; // in bytes, 0 for folders
final DateTime lastModified;
const FileItem({
required this.name,
required this.path,
required this.type,
this.size = 0,
required this.lastModified,
});
@override
List<Object?> get props => [name, path, type, size, lastModified];
FileItem copyWith({
String? name,
String? path,
FileType? type,
int? size,
DateTime? lastModified,
}) {
return FileItem(
name: name ?? this.name,
path: path ?? this.path,
type: type ?? this.type,
size: size ?? this.size,
lastModified: lastModified ?? this.lastModified,
);
}
}

View File

@@ -0,0 +1,15 @@
import 'package:equatable/equatable.dart';
class User extends Equatable {
final String email;
final String? name;
const User({required this.email, this.name});
@override
List<Object?> get props => [email, name];
User copyWith({String? email, String? name}) {
return User(email: email ?? this.email, name: name ?? this.name);
}
}

View File

@@ -0,0 +1,90 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../blocs/document_viewer/document_viewer_bloc.dart';
import '../blocs/document_viewer/document_viewer_event.dart';
import '../blocs/document_viewer/document_viewer_state.dart';
class DocumentViewer extends StatefulWidget {
final String fileId;
const DocumentViewer({super.key, required this.fileId});
@override
State<DocumentViewer> createState() => _DocumentViewerState();
}
class _DocumentViewerState extends State<DocumentViewer> {
late DocumentViewerBloc _viewerBloc;
@override
void initState() {
super.initState();
_viewerBloc = DocumentViewerBloc();
_viewerBloc.add(
OpenDocument(fileId: widget.fileId, orgId: 'org1'),
); // Assume org1
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _viewerBloc,
child: Scaffold(
appBar: AppBar(
title: BlocBuilder<DocumentViewerBloc, DocumentViewerState>(
builder: (context, state) {
if (state is ViewerReady) {
return Text(state.fileName);
}
return const Text('Document Viewer');
},
),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
_viewerBloc.add(ReloadDocument());
},
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () {
_viewerBloc.add(CloseDocument());
Navigator.of(context).pop();
},
),
],
),
body: BlocBuilder<DocumentViewerBloc, DocumentViewerState>(
builder: (context, state) {
if (state is ViewerLoading) {
return const Center(child: CircularProgressIndicator());
}
if (state is ViewerError) {
return Center(child: Text('Error: ${state.error}'));
}
if (state is ViewerReady) {
return Container(
color: Colors.grey,
child: Center(
child: Text(
'Document Viewer Placeholder\n(Backend URL: ${state.viewUrl})',
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white),
),
),
);
}
return const Center(child: Text('No document loaded'));
},
),
),
);
}
@override
void dispose() {
_viewerBloc.close();
super.dispose();
}
}

View File

@@ -0,0 +1,249 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:file_picker/file_picker.dart' hide FileType;
import 'package:go_router/go_router.dart';
import '../blocs/file_browser/file_browser_bloc.dart';
import '../blocs/file_browser/file_browser_event.dart';
import '../blocs/file_browser/file_browser_state.dart';
import '../blocs/permission/permission_bloc.dart';
import '../blocs/permission/permission_state.dart';
import '../blocs/upload/upload_bloc.dart';
import '../blocs/upload/upload_event.dart';
import '../models/file_item.dart';
class FileExplorer extends StatefulWidget {
const FileExplorer({super.key});
@override
State<FileExplorer> createState() => _FileExplorerState();
}
class _FileExplorerState extends State<FileExplorer> {
@override
void initState() {
super.initState();
// Assume org1 for now
context.read<FileBrowserBloc>().add(
LoadDirectory(orgId: 'org1', path: '/'),
);
}
@override
Widget build(BuildContext context) {
return BlocBuilder<FileBrowserBloc, FileBrowserState>(
builder: (context, state) {
if (state is DirectoryLoading) {
return const Center(child: CircularProgressIndicator());
}
if (state is DirectoryError) {
return Center(
child: Text(
'Error: ${state.error}',
style: const TextStyle(color: Colors.white),
),
);
}
if (state is DirectoryEmpty) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Drive',
style: TextStyle(fontSize: 24, color: Colors.white),
),
const SizedBox(height: 16),
// Back button
Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () {
context.read<FileBrowserBloc>().add(
LoadDirectory(orgId: 'org1', path: '/'),
);
},
),
const Text(
'Empty Folder',
style: TextStyle(color: Colors.white),
),
],
),
const SizedBox(height: 16),
BlocBuilder<PermissionBloc, PermissionState>(
builder: (context, permState) {
if (permState is PermissionLoaded &&
permState.capabilities.canWrite) {
return ElevatedButton(
onPressed: () async {
final result = await FilePicker.platform.pickFiles();
if (result != null && result.files.isNotEmpty) {
final files = result.files
.map(
(file) => FileItem(
name: file.name,
path: '/${file.name}',
type: FileType.file,
size: file.size,
lastModified: DateTime.now(),
),
)
.toList();
context.read<UploadBloc>().add(
StartUpload(
files: files,
targetPath: '/',
orgId: 'org1',
),
);
}
},
child: const Text('Upload File'),
);
}
return const SizedBox.shrink();
},
),
],
),
);
}
if (state is DirectoryLoaded) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Drive',
style: TextStyle(fontSize: 24, color: Colors.white),
),
const SizedBox(height: 16),
// Breadcrumbs and back button
Visibility(
visible: state.breadcrumbs.isNotEmpty,
child: Column(
children: [
Row(
children: [
IconButton(
icon: const Icon(
Icons.arrow_back,
color: Colors.white,
),
onPressed: () {
context.read<FileBrowserBloc>().add(
LoadDirectory(orgId: 'org1', path: '/'),
);
},
),
Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: state.breadcrumbs.map((breadcrumb) {
return TextButton(
onPressed: () {
context.read<FileBrowserBloc>().add(
NavigateToFolder(breadcrumb.path),
);
},
child: Text(
'${breadcrumb.name}/',
style: const TextStyle(
color: Colors.white70,
),
),
);
}).toList(),
),
),
),
],
),
const SizedBox(height: 16),
],
),
),
BlocBuilder<PermissionBloc, PermissionState>(
builder: (context, permState) {
if (permState is PermissionLoaded &&
permState.capabilities.canWrite) {
return ElevatedButton(
onPressed: () async {
final result = await FilePicker.platform.pickFiles();
if (result != null && result.files.isNotEmpty) {
final files = result.files
.map(
(file) => FileItem(
name: file.name,
path: '/${file.name}',
type: FileType.file,
size: file.size,
lastModified: DateTime.now(),
),
)
.toList();
context.read<UploadBloc>().add(
StartUpload(
files: files,
targetPath: '/',
orgId: 'org1',
),
);
}
},
child: const Text('Upload File'),
);
}
return const SizedBox.shrink();
},
),
const SizedBox(height: 16),
Expanded(
child: ListView.builder(
itemCount: state.files.length,
itemBuilder: (context, index) {
final file = state.files[index];
return ListTile(
leading: Icon(
file.type == FileType.folder
? Icons.folder
: Icons.insert_drive_file,
color: Colors.white,
),
title: Text(
file.name,
style: const TextStyle(color: Colors.white),
),
subtitle: Text(
file.type == FileType.folder
? 'Folder'
: 'File - ${file.size} bytes',
style: const TextStyle(color: Colors.white70),
),
onTap: () {
if (file.type == FileType.folder) {
context.read<FileBrowserBloc>().add(
NavigateToFolder(file.path),
);
} else {
// Open viewer
context.go('/viewer/${file.name}');
}
},
);
},
),
),
],
),
);
}
return const SizedBox.shrink();
},
);
}
}

View File

@@ -0,0 +1,175 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import '../blocs/auth/auth_bloc.dart';
import '../blocs/auth/auth_state.dart';
import 'login_form.dart';
import 'file_explorer.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
late String _selectedTab = 'Drive';
late AnimationController _animationController;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 400),
vsync: this,
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
Center(
child: BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
final isLoggedIn = state is AuthAuthenticated;
if (isLoggedIn && !_animationController.isAnimating) {
_animationController.forward();
} else if (!isLoggedIn) {
_animationController.reverse();
}
return AnimatedContainer(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
width: isLoggedIn
? MediaQuery.of(context).size.width * 0.9
: 340,
height: isLoggedIn
? MediaQuery.of(context).size.height * 0.9
: 280,
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Container(
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Colors.white.withValues(alpha: 0.2),
),
),
child: isLoggedIn
? const FileExplorer()
: const LoginForm(),
),
),
),
);
},
),
),
Positioned(
top: 10,
left: 0,
right: 0,
child: Center(
child: Text(
'b0esche.cloud',
style: TextStyle(
fontFamily: 'PixelatedElegance',
fontSize: 42,
color: Colors.white,
decoration: TextDecoration.underline,
decorationColor: Colors.white,
fontFeatures: [const FontFeature.slashedZero()],
),
),
),
),
Positioned(
top: 10,
right: 20,
child: BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
final isLoggedIn = state is AuthAuthenticated;
if (!isLoggedIn) {
return const SizedBox.shrink();
}
return ScaleTransition(
scale: Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.easeOutBack,
),
),
child: Row(
children: [
_buildNavButton('Drive', Icons.cloud),
const SizedBox(width: 16),
_buildNavButton('Mail', Icons.mail),
const SizedBox(width: 16),
_buildNavButton('Add', Icons.add),
const SizedBox(width: 16),
_buildNavButton('Profile', Icons.person, isAvatar: true),
],
),
);
},
),
),
],
),
);
}
Widget _buildNavButton(String label, IconData icon, {bool isAvatar = false}) {
final isSelected = _selectedTab == label;
final highlightColor = Color.fromARGB(255, 100, 200, 255);
final defaultColor = Colors.white70;
return GestureDetector(
onTap: () {
setState(() {
_selectedTab = label;
});
},
child: isAvatar
? CircleAvatar(
backgroundColor: isSelected ? highlightColor : defaultColor,
child: Icon(icon, color: Colors.black),
)
: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
color: isSelected ? highlightColor : defaultColor,
size: 24,
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
color: isSelected ? highlightColor : defaultColor,
fontSize: 12,
fontWeight: isSelected
? FontWeight.bold
: FontWeight.normal,
),
),
],
),
);
}
}

View File

@@ -0,0 +1,97 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../blocs/auth/auth_bloc.dart';
import '../blocs/auth/auth_event.dart';
import '../blocs/auth/auth_state.dart';
import '../blocs/session/session_bloc.dart';
import '../blocs/session/session_event.dart';
class LoginForm extends StatefulWidget {
const LoginForm({super.key});
@override
State<LoginForm> createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
@override
Widget build(BuildContext context) {
return BlocListener<AuthBloc, AuthState>(
listener: (context, state) {
if (state is AuthFailure) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(state.error)));
} else if (state is AuthAuthenticated) {
// Start session
context.read<SessionBloc>().add(SessionStarted(state.token));
}
},
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'sign in',
style: TextStyle(fontSize: 24, color: Colors.white),
),
const SizedBox(height: 16),
TextField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'email',
labelStyle: TextStyle(color: Colors.white),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.white),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.white),
),
),
style: const TextStyle(color: Colors.white),
),
const SizedBox(height: 16),
TextField(
controller: _passwordController,
decoration: const InputDecoration(
labelText: 'password',
labelStyle: TextStyle(color: Colors.white),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.white),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.white),
),
),
obscureText: true,
style: const TextStyle(color: Colors.white),
),
const SizedBox(height: 16),
BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
if (state is AuthLoading) {
return const CircularProgressIndicator();
}
return ElevatedButton(
onPressed: () {
context.read<AuthBloc>().add(
LoginRequested(
_emailController.text,
_passwordController.text,
),
);
},
child: const Text('sign in'),
);
},
),
],
),
),
);
}
}

View File

@@ -0,0 +1,7 @@
import '../models/user.dart';
abstract class AuthRepository {
Future<User> login(String email, String password);
Future<void> logout();
Future<User?> getCurrentUser();
}

View File

@@ -0,0 +1,8 @@
import '../models/file_item.dart';
abstract class FileRepository {
Future<List<FileItem>> getFiles(String orgId, String path);
Future<FileItem?> getFile(String orgId, String path);
Future<void> uploadFile(String orgId, FileItem file);
Future<void> deleteFile(String orgId, String path);
}

View File

@@ -0,0 +1,25 @@
import '../models/user.dart';
import '../repositories/auth_repository.dart';
class MockAuthRepository implements AuthRepository {
@override
Future<User> login(String email, String password) async {
await Future.delayed(const Duration(seconds: 1));
if (email.isNotEmpty && password.isNotEmpty) {
return User(email: email);
} else {
throw Exception('Invalid credentials');
}
}
@override
Future<void> logout() async {
// Mock logout
}
@override
Future<User?> getCurrentUser() async {
// Mock: return null or a user
return null;
}
}

View File

@@ -0,0 +1,63 @@
import '../models/file_item.dart';
import '../repositories/file_repository.dart';
class MockFileRepository implements FileRepository {
final List<FileItem> _files = [
FileItem(
name: 'Documents',
path: '/Documents',
type: FileType.folder,
lastModified: DateTime.now(),
),
FileItem(
name: 'Images',
path: '/Images',
type: FileType.folder,
lastModified: DateTime.now(),
),
FileItem(
name: 'report.pdf',
path: '/report.pdf',
type: FileType.file,
size: 1024,
lastModified: DateTime.now(),
),
FileItem(
name: 'code.dart',
path: '/code.dart',
type: FileType.file,
size: 512,
lastModified: DateTime.now(),
),
];
@override
Future<List<FileItem>> getFiles(String orgId, String path) async {
await Future.delayed(const Duration(seconds: 1));
if (path == '/') {
return _files;
} else {
// For subfolders, return empty for simplicity
return [];
}
}
@override
Future<FileItem?> getFile(String orgId, String path) async {
return _files.firstWhere(
(f) => f.path == path,
orElse: () => null as FileItem,
);
}
@override
Future<void> uploadFile(String orgId, FileItem file) async {
await Future.delayed(const Duration(seconds: 1));
_files.add(file);
}
@override
Future<void> deleteFile(String orgId, String path) async {
_files.removeWhere((f) => f.path == path);
}
}

View File

@@ -0,0 +1,26 @@
import '../models/user.dart';
import '../repositories/auth_repository.dart';
class AuthService {
final AuthRepository _authRepository;
AuthService(this._authRepository);
Future<User> login(String email, String password) async {
if (email.isEmpty || password.isEmpty) {
throw Exception('Email and password are required');
}
if (!email.contains('@')) {
throw Exception('Invalid email format');
}
return await _authRepository.login(email, password);
}
Future<void> logout() async {
await _authRepository.logout();
}
Future<User?> getCurrentUser() async {
return await _authRepository.getCurrentUser();
}
}

View File

@@ -0,0 +1,36 @@
import '../models/file_item.dart';
import '../repositories/file_repository.dart';
class FileService {
final FileRepository _fileRepository;
FileService(this._fileRepository);
Future<List<FileItem>> getFiles(String orgId, String path) async {
if (path.isEmpty) {
throw Exception('Path cannot be empty');
}
return await _fileRepository.getFiles(orgId, path);
}
Future<FileItem?> getFile(String orgId, String path) async {
if (path.isEmpty) {
return null;
}
return await _fileRepository.getFile(orgId, path);
}
Future<void> uploadFile(String orgId, FileItem file) async {
if (file.name.isEmpty) {
throw Exception('File name cannot be empty');
}
await _fileRepository.uploadFile(orgId, file);
}
Future<void> deleteFile(String orgId, String path) async {
if (path.isEmpty) {
throw Exception('Path cannot be empty');
}
await _fileRepository.deleteFile(orgId, path);
}
}

View File

@@ -0,0 +1,56 @@
import 'package:flutter/foundation.dart';
import '../models/file_item.dart';
import '../services/file_service.dart';
class FileExplorerViewModel extends ChangeNotifier {
final FileService _fileService;
FileExplorerViewModel(this._fileService);
List<FileItem> _files = [];
bool _isLoading = false;
String? _error;
String _currentPath = '/';
List<FileItem> get files => _files;
bool get isLoading => _isLoading;
String? get error => _error;
String get currentPath => _currentPath;
Future<void> loadFiles([String? path]) async {
_isLoading = true;
_error = null;
if (path != null) _currentPath = path;
notifyListeners();
try {
_files = await _fileService.getFiles("", _currentPath);
} catch (e) {
_error = e.toString();
_files = [];
} finally {
_isLoading = false;
notifyListeners();
}
}
Future<void> uploadFile(FileItem file) async {
try {
await _fileService.uploadFile("", file);
await loadFiles(); // Reload files
} catch (e) {
_error = e.toString();
notifyListeners();
}
}
Future<void> deleteFile(String path) async {
try {
await _fileService.deleteFile("", path);
await loadFiles(); // Reload files
} catch (e) {
_error = e.toString();
notifyListeners();
}
}
}

View File

@@ -0,0 +1,65 @@
import 'package:flutter/foundation.dart';
import '../models/user.dart';
import '../services/auth_service.dart';
class LoginViewModel extends ChangeNotifier {
final AuthService _authService;
LoginViewModel(this._authService);
bool _isLoading = false;
String? _error;
User? _currentUser;
bool get isLoading => _isLoading;
String? get error => _error;
User? get currentUser => _currentUser;
bool get isLoggedIn => _currentUser != null;
Future<void> login(String email, String password) async {
_isLoading = true;
_error = null;
notifyListeners();
try {
_currentUser = await _authService.login(email, password);
_error = null;
} catch (e) {
_error = e.toString();
_currentUser = null;
} finally {
_isLoading = false;
notifyListeners();
}
}
Future<void> logout() async {
_isLoading = true;
notifyListeners();
try {
await _authService.logout();
_currentUser = null;
_error = null;
} catch (e) {
_error = e.toString();
} finally {
_isLoading = false;
notifyListeners();
}
}
Future<void> checkCurrentUser() async {
_isLoading = true;
notifyListeners();
try {
_currentUser = await _authService.getCurrentUser();
} catch (e) {
_error = e.toString();
} finally {
_isLoading = false;
notifyListeners();
}
}
}

View File

@@ -6,6 +6,26 @@
#include "generated_plugin_registrant.h"
#include <desktop_drop/desktop_drop_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <irondash_engine_context/irondash_engine_context_plugin.h>
#include <super_native_extensions/super_native_extensions_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) desktop_drop_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin");
desktop_drop_plugin_register_with_registrar(desktop_drop_registrar);
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
g_autoptr(FlPluginRegistrar) irondash_engine_context_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "IrondashEngineContextPlugin");
irondash_engine_context_plugin_register_with_registrar(irondash_engine_context_registrar);
g_autoptr(FlPluginRegistrar) super_native_extensions_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "SuperNativeExtensionsPlugin");
super_native_extensions_plugin_register_with_registrar(super_native_extensions_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
}

View File

@@ -3,6 +3,11 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
desktop_drop
flutter_secure_storage_linux
irondash_engine_context
super_native_extensions
url_launcher_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"

View File

@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"

View File

@@ -5,6 +5,30 @@
import FlutterMacOS
import Foundation
import connectivity_plus
import desktop_drop
import device_info_plus
import file_picker
import flutter_secure_storage_macos
import irondash_engine_context
import path_provider_foundation
import shared_preferences_foundation
import sqflite_darwin
import super_native_extensions
import syncfusion_pdfviewer_macos
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin"))
DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
SuperNativeExtensionsPlugin.register(with: registry.registrar(forPlugin: "SuperNativeExtensionsPlugin"))
SyncfusionFlutterPdfViewerPlugin.register(with: registry.registrar(forPlugin: "SyncfusionFlutterPdfViewerPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}

View File

@@ -0,0 +1,42 @@
platform :osx, '10.15'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_macos_podfile_setup
target 'Runner' do
use_frameworks!
flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_macos_build_settings(target)
end
end

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
name: b0esche_cloud
description: "A new Flutter project."
publish_to: 'none'
publish_to: "none"
version: 0.1.0
environment:
@@ -10,10 +10,85 @@ dependencies:
flutter:
sdk: flutter
# State Management
flutter_bloc: ^8.1.3
bloc: ^8.1.2
# Networking
dio: ^5.3.2
# Routing
go_router: ^12.1.1
# Dependency Injection
get_it: ^7.6.4
injectable: ^2.3.2
# Local Storage
hive: ^2.2.3
hive_flutter: ^1.1.0
shared_preferences: ^2.2.2
flutter_secure_storage: ^9.0.0
# Internationalization
intl: ^0.19.0
# Logging
logger: ^2.0.2
# Image Handling
cached_network_image: ^3.3.0
# SVG Support
flutter_svg: ^2.0.9
# Utilities
equatable: ^2.0.5
path_provider: ^2.1.2
connectivity_plus: ^5.0.2
provider: ^6.1.1
file_picker: ^8.1.2
flutter_dropzone: ^4.0.0
desktop_drop: ^0.4.4
super_context_menu: ^0.8.5
infinite_scroll_pagination: ^4.0.0
collection: ^1.18.0
syncfusion_flutter_pdfviewer: ^24.2.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
# Code Generation
build_runner: ^2.4.6
json_serializable: ^6.7.1
injectable_generator: ^2.4.1
hive_generator: ^2.0.1
# Testing
mockito: ^5.4.4
bloc_test: ^9.1.5
flutter:
uses-material-design: true
assets:
- assets/fonts/
fonts:
- family: VeteranTypewriter
fonts:
- asset: assets/fonts/veteran-typewriter/veteran_typewriter.ttf
- family: AnimalPark
fonts:
- asset: assets/fonts/animal-park/animal_park.otf
- family: RenoireDemo
fonts:
- asset: assets/fonts/renoire-demo/renoire_demo.otf
- family: SparkyStones
fonts:
- asset: assets/fonts/sparky-stones/SparkyStonesRegular-BW6ld.ttf
- family: PixelatedElegance
fonts:
- asset: assets/fonts/pixelated-elegance/PixelatedEleganceRegular-ovyAA.ttf

View File

@@ -1,5 +1,6 @@
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
@@ -29,10 +30,35 @@
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png" />
<!-- Preload fonts -->
<link rel="preload" href="assets/fonts/veteran-typewriter/veteran_typewriter.ttf" as="font" type="font/ttf"
crossorigin>
<link rel="preload" href="assets/fonts/animal-park/animal_park.otf" as="font" type="font/otf" crossorigin>
<link rel="preload" href="assets/fonts/renoire-demo/renoire_demo.otf" as="font" type="font/otf" crossorigin>
<style>
@font-face {
font-family: 'VeteranTypewriter';
src: url('assets/fonts/veteran-typewriter/veteran_typewriter.ttf') format('truetype');
}
@font-face {
font-family: 'AnimalPark';
src: url('assets/fonts/animal-park/animal_park.otf') format('opentype');
}
@font-face {
font-family: 'RenoireDemo';
src: url('assets/fonts/renoire-demo/renoire_demo.otf') format('opentype');
}
</style>
<title>b0esche_cloud</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
<script src="flutter_bootstrap.js" async></script>
</body>
</html>

View File

@@ -6,6 +6,27 @@
#include "generated_plugin_registrant.h"
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <desktop_drop/desktop_drop_plugin.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <irondash_engine_context/irondash_engine_context_plugin_c_api.h>
#include <super_native_extensions/super_native_extensions_plugin_c_api.h>
#include <syncfusion_pdfviewer_windows/syncfusion_pdfviewer_windows_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
DesktopDropPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("DesktopDropPlugin"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
IrondashEngineContextPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("IrondashEngineContextPluginCApi"));
SuperNativeExtensionsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SuperNativeExtensionsPluginCApi"));
SyncfusionPdfviewerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SyncfusionPdfviewerWindowsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}

View File

@@ -3,6 +3,13 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
connectivity_plus
desktop_drop
flutter_secure_storage_windows
irondash_engine_context
super_native_extensions
syncfusion_pdfviewer_windows
url_launcher_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST