feat: deploy infrastructure + disk/drive, calendar, presentation, workspaces, onboarding, demo user
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../core/api/client.dart';
|
||||
import '../core/api/endpoints.dart';
|
||||
import '../core/api/exceptions.dart';
|
||||
import '../models/auth.dart';
|
||||
|
||||
enum AuthStatus { initial, authenticated, unauthenticated, loading }
|
||||
|
||||
class AuthState {
|
||||
final AuthStatus status;
|
||||
final User? user;
|
||||
final String? error;
|
||||
final bool needsTwoFactor;
|
||||
|
||||
AuthState({
|
||||
this.status = AuthStatus.initial,
|
||||
this.user,
|
||||
this.error,
|
||||
this.needsTwoFactor = false,
|
||||
});
|
||||
|
||||
AuthState copyWith({
|
||||
AuthStatus? status,
|
||||
User? user,
|
||||
String? error,
|
||||
bool? needsTwoFactor,
|
||||
}) =>
|
||||
AuthState(
|
||||
status: status ?? this.status,
|
||||
user: user ?? this.user,
|
||||
error: error,
|
||||
needsTwoFactor: needsTwoFactor ?? this.needsTwoFactor,
|
||||
);
|
||||
}
|
||||
|
||||
class AuthNotifier extends StateNotifier<AuthState> {
|
||||
final ApiClient _api;
|
||||
|
||||
AuthNotifier(this._api) : super(AuthState()) {
|
||||
_checkAuth();
|
||||
}
|
||||
|
||||
Future<void> _checkAuth() async {
|
||||
final token = await _api.getAccessToken();
|
||||
if (token != null) {
|
||||
state = state.copyWith(status: AuthStatus.authenticated);
|
||||
} else {
|
||||
state = state.copyWith(status: AuthStatus.unauthenticated);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> login(String email, String password) async {
|
||||
state = state.copyWith(status: AuthStatus.loading, error: null);
|
||||
try {
|
||||
final response = await _api.post(Endpoints.login, data: {
|
||||
'email': email,
|
||||
'password': password,
|
||||
});
|
||||
final authResponse = AuthResponse.fromJson(
|
||||
response.data as Map<String, dynamic>);
|
||||
if (authResponse.user.isTwoFactorEnabled) {
|
||||
state = state.copyWith(
|
||||
status: AuthStatus.unauthenticated,
|
||||
needsTwoFactor: true,
|
||||
);
|
||||
return;
|
||||
}
|
||||
await _api.saveTokens(
|
||||
authResponse.accessToken, authResponse.refreshToken);
|
||||
state = state.copyWith(
|
||||
status: AuthStatus.authenticated,
|
||||
user: authResponse.user,
|
||||
);
|
||||
} on ApiException catch (e) {
|
||||
state = state.copyWith(
|
||||
status: AuthStatus.unauthenticated,
|
||||
error: e.message,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> register(String email, String password, String name) async {
|
||||
state = state.copyWith(status: AuthStatus.loading, error: null);
|
||||
try {
|
||||
final response = await _api.post(Endpoints.register, data: {
|
||||
'email': email,
|
||||
'password': password,
|
||||
'name': name,
|
||||
});
|
||||
final authResponse = AuthResponse.fromJson(
|
||||
response.data as Map<String, dynamic>);
|
||||
await _api.saveTokens(
|
||||
authResponse.accessToken, authResponse.refreshToken);
|
||||
state = state.copyWith(
|
||||
status: AuthStatus.authenticated,
|
||||
user: authResponse.user,
|
||||
);
|
||||
} on ApiException catch (e) {
|
||||
state = state.copyWith(
|
||||
status: AuthStatus.unauthenticated,
|
||||
error: e.message,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> verifyTwoFactor(String code) async {
|
||||
state = state.copyWith(status: AuthStatus.loading, error: null);
|
||||
try {
|
||||
final response = await _api.post(Endpoints.twoFactor, data: {
|
||||
'code': code,
|
||||
});
|
||||
final authResponse = AuthResponse.fromJson(
|
||||
response.data as Map<String, dynamic>);
|
||||
await _api.saveTokens(
|
||||
authResponse.accessToken, authResponse.refreshToken);
|
||||
state = state.copyWith(
|
||||
status: AuthStatus.authenticated,
|
||||
user: authResponse.user,
|
||||
needsTwoFactor: false,
|
||||
);
|
||||
} on ApiException catch (e) {
|
||||
state = state.copyWith(
|
||||
status: AuthStatus.unauthenticated,
|
||||
error: e.message,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
await _api.clearTokens();
|
||||
state = AuthState(status: AuthStatus.unauthenticated);
|
||||
}
|
||||
|
||||
Future<void> forgotPassword(String email) async {
|
||||
state = state.copyWith(status: AuthStatus.loading, error: null);
|
||||
try {
|
||||
await _api.post(Endpoints.forgotPassword, data: {'email': email});
|
||||
state = state.copyWith(status: AuthStatus.unauthenticated);
|
||||
} on ApiException catch (e) {
|
||||
state = state.copyWith(
|
||||
status: AuthStatus.unauthenticated, error: e.message);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> resetPassword(String token, String password) async {
|
||||
state = state.copyWith(status: AuthStatus.loading, error: null);
|
||||
try {
|
||||
await _api.post(Endpoints.resetPassword, data: {
|
||||
'token': token,
|
||||
'password': password,
|
||||
});
|
||||
state = state.copyWith(status: AuthStatus.unauthenticated);
|
||||
} on ApiException catch (e) {
|
||||
state = state.copyWith(
|
||||
status: AuthStatus.unauthenticated, error: e.message);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> getOAuthUrl(String provider) async {
|
||||
try {
|
||||
final response = await _api.get('${Endpoints.oauth}/$provider');
|
||||
return response.data['url'] as String?;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void clearError() {
|
||||
state = state.copyWith(error: null);
|
||||
}
|
||||
}
|
||||
|
||||
final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
|
||||
final api = ref.read(apiClientProvider);
|
||||
return AuthNotifier(api);
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../api/disk.dart';
|
||||
import '../core/api/client.dart';
|
||||
|
||||
class DiskState {
|
||||
final List<String> availableProviders;
|
||||
final List<Map<String, dynamic>> connectedProviders;
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
|
||||
DiskState({
|
||||
this.availableProviders = const [],
|
||||
this.connectedProviders = const [],
|
||||
this.isLoading = false,
|
||||
this.error,
|
||||
});
|
||||
|
||||
DiskState copyWith({
|
||||
List<String>? availableProviders,
|
||||
List<Map<String, dynamic>>? connectedProviders,
|
||||
bool? isLoading,
|
||||
String? error,
|
||||
}) =>
|
||||
DiskState(
|
||||
availableProviders: availableProviders ?? this.availableProviders,
|
||||
connectedProviders: connectedProviders ?? this.connectedProviders,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
error: error,
|
||||
);
|
||||
|
||||
bool isConnected(String provider) =>
|
||||
connectedProviders.any((p) => p['provider'] == provider);
|
||||
}
|
||||
|
||||
class DiskNotifier extends StateNotifier<DiskState> {
|
||||
final DiskApi _api;
|
||||
|
||||
DiskNotifier(this._api) : super(DiskState());
|
||||
|
||||
Future<void> load() async {
|
||||
state = state.copyWith(isLoading: true, error: null);
|
||||
try {
|
||||
final providers = await _api.getProviders();
|
||||
final status = await _api.getStatus();
|
||||
final connected = (status['connected'] as List<dynamic>? ?? [])
|
||||
.map((e) => e as Map<String, dynamic>)
|
||||
.toList();
|
||||
state = state.copyWith(
|
||||
availableProviders: providers,
|
||||
connectedProviders: connected,
|
||||
isLoading: false,
|
||||
);
|
||||
} catch (e) {
|
||||
state = state.copyWith(isLoading: false, error: e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> connect(String provider, String code) async {
|
||||
final ok = await _api.connect(provider, code);
|
||||
if (ok) await load();
|
||||
return ok;
|
||||
}
|
||||
|
||||
Future<bool> disconnect(String provider) async {
|
||||
final ok = await _api.disconnect(provider);
|
||||
if (ok) {
|
||||
state = state.copyWith(
|
||||
connectedProviders: state.connectedProviders
|
||||
.where((p) => p['provider'] != provider)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
Future<String?> getOAuthUrl(String provider) =>
|
||||
_api.getOAuthUrl(provider);
|
||||
}
|
||||
|
||||
final diskProvider = StateNotifierProvider<DiskNotifier, DiskState>((ref) {
|
||||
final api = ref.read(apiClientProvider);
|
||||
return DiskNotifier(DiskApi(api));
|
||||
});
|
||||
@@ -0,0 +1,269 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../core/api/client.dart';
|
||||
import '../core/api/endpoints.dart';
|
||||
import '../core/api/exceptions.dart';
|
||||
import '../models/idea.dart';
|
||||
import '../models/collaboration.dart';
|
||||
|
||||
class IdeasState {
|
||||
final List<Idea> ideas;
|
||||
final Idea? selectedIdea;
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
final String viewMode;
|
||||
|
||||
IdeasState({
|
||||
this.ideas = const [],
|
||||
this.selectedIdea,
|
||||
this.isLoading = false,
|
||||
this.error,
|
||||
this.viewMode = 'list',
|
||||
});
|
||||
|
||||
IdeasState copyWith({
|
||||
List<Idea>? ideas,
|
||||
Idea? selectedIdea,
|
||||
bool? isLoading,
|
||||
String? error,
|
||||
String? viewMode,
|
||||
}) =>
|
||||
IdeasState(
|
||||
ideas: ideas ?? this.ideas,
|
||||
selectedIdea: selectedIdea ?? this.selectedIdea,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
error: error,
|
||||
viewMode: viewMode ?? this.viewMode,
|
||||
);
|
||||
}
|
||||
|
||||
class IdeasNotifier extends StateNotifier<IdeasState> {
|
||||
final ApiClient _api;
|
||||
|
||||
IdeasNotifier(this._api) : super(IdeasState());
|
||||
|
||||
Future<void> loadIdeas({String? workspaceId}) async {
|
||||
state = state.copyWith(isLoading: true, error: null);
|
||||
try {
|
||||
final params = <String, dynamic>{};
|
||||
if (workspaceId != null) params['workspace_id'] = workspaceId;
|
||||
final response = await _api.get(Endpoints.ideas, params: params);
|
||||
final list = (response.data as List<dynamic>)
|
||||
.map((e) => Idea.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
state = state.copyWith(ideas: list, isLoading: false);
|
||||
} on ApiException catch (e) {
|
||||
state = state.copyWith(isLoading: false, error: e.message);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadIdea(String id) async {
|
||||
state = state.copyWith(isLoading: true, error: null);
|
||||
try {
|
||||
final response = await _api.get(Endpoints.idea(id));
|
||||
final idea =
|
||||
Idea.fromJson(response.data as Map<String, dynamic>);
|
||||
state = state.copyWith(selectedIdea: idea, isLoading: false);
|
||||
} on ApiException catch (e) {
|
||||
state = state.copyWith(isLoading: false, error: e.message);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> createIdea(CreateIdeaRequest request) async {
|
||||
try {
|
||||
final response = await _api.post(Endpoints.ideas, data: request.toJson());
|
||||
final idea =
|
||||
Idea.fromJson(response.data as Map<String, dynamic>);
|
||||
state = state.copyWith(
|
||||
ideas: [idea, ...state.ideas],
|
||||
selectedIdea: idea,
|
||||
);
|
||||
return true;
|
||||
} on ApiException catch (e) {
|
||||
state = state.copyWith(error: e.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> updateIdea(String id, UpdateIdeaRequest request) async {
|
||||
try {
|
||||
final response =
|
||||
await _api.patch(Endpoints.idea(id), data: request.toJson());
|
||||
final updated =
|
||||
Idea.fromJson(response.data as Map<String, dynamic>);
|
||||
state = state.copyWith(
|
||||
ideas: state.ideas.map((i) => i.id == id ? updated : i).toList(),
|
||||
selectedIdea: updated,
|
||||
);
|
||||
return true;
|
||||
} on ApiException catch (e) {
|
||||
state = state.copyWith(error: e.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> deleteIdea(String id) async {
|
||||
try {
|
||||
await _api.delete(Endpoints.idea(id));
|
||||
state = state.copyWith(
|
||||
ideas: state.ideas.where((i) => i.id != id).toList(),
|
||||
selectedIdea: null,
|
||||
);
|
||||
return true;
|
||||
} on ApiException catch (e) {
|
||||
state = state.copyWith(error: e.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> updateFunnel(String ideaId, String status) async {
|
||||
try {
|
||||
final response = await _api.post(Endpoints.ideaFunnel(ideaId), data: {
|
||||
'status': status,
|
||||
});
|
||||
final updated =
|
||||
Idea.fromJson(response.data as Map<String, dynamic>);
|
||||
state = state.copyWith(
|
||||
ideas: state.ideas.map((i) => i.id == ideaId ? updated : i).toList(),
|
||||
selectedIdea: updated,
|
||||
);
|
||||
return true;
|
||||
} on ApiException catch (e) {
|
||||
state = state.copyWith(error: e.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void setViewMode(String mode) {
|
||||
state = state.copyWith(viewMode: mode);
|
||||
}
|
||||
|
||||
void clearError() {
|
||||
state = state.copyWith(error: null);
|
||||
}
|
||||
}
|
||||
|
||||
final ideasProvider =
|
||||
StateNotifierProvider<IdeasNotifier, IdeasState>((ref) {
|
||||
final api = ref.read(apiClientProvider);
|
||||
return IdeasNotifier(api);
|
||||
});
|
||||
|
||||
class IdeaCommentsState {
|
||||
final List<Comment> comments;
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
|
||||
IdeaCommentsState({
|
||||
this.comments = const [],
|
||||
this.isLoading = false,
|
||||
this.error,
|
||||
});
|
||||
|
||||
IdeaCommentsState copyWith({
|
||||
List<Comment>? comments,
|
||||
bool? isLoading,
|
||||
String? error,
|
||||
}) =>
|
||||
IdeaCommentsState(
|
||||
comments: comments ?? this.comments,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
error: error,
|
||||
);
|
||||
}
|
||||
|
||||
class IdeaCommentsNotifier extends StateNotifier<IdeaCommentsState> {
|
||||
final ApiClient _api;
|
||||
final String ideaId;
|
||||
|
||||
IdeaCommentsNotifier(this._api, this.ideaId) : super(IdeaCommentsState());
|
||||
|
||||
Future<void> loadComments() async {
|
||||
state = state.copyWith(isLoading: true);
|
||||
try {
|
||||
final response = await _api.get(Endpoints.ideaComments(ideaId));
|
||||
final list = (response.data as List<dynamic>)
|
||||
.map((e) => Comment.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
state = state.copyWith(comments: list, isLoading: false);
|
||||
} on ApiException catch (e) {
|
||||
state = state.copyWith(isLoading: false, error: e.message);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> addComment(String body, {String? parentId}) async {
|
||||
try {
|
||||
final response = await _api.post(Endpoints.ideaComments(ideaId), data: {
|
||||
'body': body,
|
||||
if (parentId != null) 'parent_id': parentId,
|
||||
});
|
||||
final comment =
|
||||
Comment.fromJson(response.data as Map<String, dynamic>);
|
||||
state = state.copyWith(comments: [...state.comments, comment]);
|
||||
return true;
|
||||
} on ApiException catch (e) {
|
||||
state = state.copyWith(error: e.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> toggleVote(String ideaId) async {
|
||||
try {
|
||||
await _api.post(Endpoints.ideaVote(ideaId));
|
||||
} on ApiException catch (e) {
|
||||
state = state.copyWith(error: e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final ideaCommentsProvider =
|
||||
StateNotifierProvider.family<IdeaCommentsNotifier, IdeaCommentsState, String>(
|
||||
(ref, ideaId) {
|
||||
final api = ref.read(apiClientProvider);
|
||||
return IdeaCommentsNotifier(api, ideaId);
|
||||
});
|
||||
|
||||
class IdeaActivityState {
|
||||
final List<Activity> activities;
|
||||
final bool isLoading;
|
||||
|
||||
IdeaActivityState({
|
||||
this.activities = const [],
|
||||
this.isLoading = false,
|
||||
});
|
||||
|
||||
IdeaActivityState copyWith({
|
||||
List<Activity>? activities,
|
||||
bool? isLoading,
|
||||
}) =>
|
||||
IdeaActivityState(
|
||||
activities: activities ?? this.activities,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
);
|
||||
}
|
||||
|
||||
class IdeaActivityNotifier extends StateNotifier<IdeaActivityState> {
|
||||
final ApiClient _api;
|
||||
final String ideaId;
|
||||
|
||||
IdeaActivityNotifier(this._api, this.ideaId) : super(IdeaActivityState());
|
||||
|
||||
Future<void> loadActivities() async {
|
||||
state = state.copyWith(isLoading: true);
|
||||
try {
|
||||
final response = await _api.get(Endpoints.ideaActivity(ideaId));
|
||||
final list = (response.data as List<dynamic>)
|
||||
.map((e) => Activity.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
state = state.copyWith(activities: list, isLoading: false);
|
||||
} catch (_) {
|
||||
state = state.copyWith(isLoading: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final ideaActivityProvider =
|
||||
StateNotifierProvider.family<IdeaActivityNotifier, IdeaActivityState, String>(
|
||||
(ref, ideaId) {
|
||||
final api = ref.read(apiClientProvider);
|
||||
return IdeaActivityNotifier(api, ideaId);
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../core/api/client.dart';
|
||||
import '../core/api/endpoints.dart';
|
||||
import '../core/api/exceptions.dart';
|
||||
import '../models/collaboration.dart';
|
||||
|
||||
class NotificationState {
|
||||
final List<AppNotification> notifications;
|
||||
final int unreadCount;
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
|
||||
NotificationState({
|
||||
this.notifications = const [],
|
||||
this.unreadCount = 0,
|
||||
this.isLoading = false,
|
||||
this.error,
|
||||
});
|
||||
|
||||
NotificationState copyWith({
|
||||
List<AppNotification>? notifications,
|
||||
int? unreadCount,
|
||||
bool? isLoading,
|
||||
String? error,
|
||||
}) =>
|
||||
NotificationState(
|
||||
notifications: notifications ?? this.notifications,
|
||||
unreadCount: unreadCount ?? this.unreadCount,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
error: error,
|
||||
);
|
||||
}
|
||||
|
||||
class NotificationNotifier extends StateNotifier<NotificationState> {
|
||||
final ApiClient _api;
|
||||
|
||||
NotificationNotifier(this._api) : super(NotificationState());
|
||||
|
||||
Future<void> loadNotifications() async {
|
||||
state = state.copyWith(isLoading: true);
|
||||
try {
|
||||
final response = await _api.get(Endpoints.notifications);
|
||||
final list = (response.data as List<dynamic>)
|
||||
.map((e) =>
|
||||
AppNotification.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
state = state.copyWith(notifications: list, isLoading: false);
|
||||
_updateUnreadCount();
|
||||
} on ApiException catch (e) {
|
||||
state = state.copyWith(isLoading: false, error: e.message);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadUnreadCount() async {
|
||||
try {
|
||||
final response =
|
||||
await _api.get(Endpoints.notificationsUnreadCount);
|
||||
state = state.copyWith(
|
||||
unreadCount: response.data['count'] as int? ?? 0);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<void> markAsRead(String id) async {
|
||||
try {
|
||||
await _api.patch(Endpoints.notification(id));
|
||||
state = state.copyWith(
|
||||
notifications: state.notifications.map((n) {
|
||||
if (n.id == id) {
|
||||
return AppNotification(
|
||||
id: n.id,
|
||||
userId: n.userId,
|
||||
title: n.title,
|
||||
body: n.body,
|
||||
isRead: true,
|
||||
ideaId: n.ideaId,
|
||||
createdAt: n.createdAt,
|
||||
);
|
||||
}
|
||||
return n;
|
||||
}).toList(),
|
||||
);
|
||||
_updateUnreadCount();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<void> markAllAsRead() async {
|
||||
try {
|
||||
await _api.post(Endpoints.notificationsReadAll);
|
||||
state = state.copyWith(
|
||||
notifications: state.notifications
|
||||
.map((n) => AppNotification(
|
||||
id: n.id,
|
||||
userId: n.userId,
|
||||
title: n.title,
|
||||
body: n.body,
|
||||
isRead: true,
|
||||
ideaId: n.ideaId,
|
||||
createdAt: n.createdAt,
|
||||
))
|
||||
.toList(),
|
||||
unreadCount: 0,
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
void _updateUnreadCount() {
|
||||
state = state.copyWith(
|
||||
unreadCount: state.notifications.where((n) => !n.isRead).length,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final notificationProvider = StateNotifierProvider<NotificationNotifier, NotificationState>((ref) {
|
||||
final api = ref.read(apiClientProvider);
|
||||
return NotificationNotifier(api);
|
||||
});
|
||||
|
||||
final unreadCountProvider = Provider<int>((ref) {
|
||||
return ref.watch(notificationProvider).unreadCount;
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
final themeModeProvider =
|
||||
StateNotifierProvider<ThemeModeNotifier, ThemeMode>((ref) {
|
||||
return ThemeModeNotifier();
|
||||
});
|
||||
|
||||
class ThemeModeNotifier extends StateNotifier<ThemeMode> {
|
||||
ThemeModeNotifier() : super(ThemeMode.system) {
|
||||
_load();
|
||||
}
|
||||
|
||||
final _storage = const FlutterSecureStorage();
|
||||
static const _key = 'theme_mode';
|
||||
|
||||
Future<void> _load() async {
|
||||
final value = await _storage.read(key: _key);
|
||||
if (value == 'light') state = ThemeMode.light;
|
||||
if (value == 'dark') state = ThemeMode.dark;
|
||||
}
|
||||
|
||||
Future<void> setTheme(ThemeMode mode) async {
|
||||
state = mode;
|
||||
final value = mode == ThemeMode.light
|
||||
? 'light'
|
||||
: mode == ThemeMode.dark
|
||||
? 'dark'
|
||||
: 'system';
|
||||
await _storage.write(key: _key, value: value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../core/api/client.dart';
|
||||
import '../core/api/endpoints.dart';
|
||||
import '../core/api/exceptions.dart';
|
||||
import '../models/workspace.dart';
|
||||
|
||||
class WorkspaceState {
|
||||
final List<Workspace> workspaces;
|
||||
final Workspace? selectedWorkspace;
|
||||
final List<WorkspaceMember> members;
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
|
||||
WorkspaceState({
|
||||
this.workspaces = const [],
|
||||
this.selectedWorkspace,
|
||||
this.members = const [],
|
||||
this.isLoading = false,
|
||||
this.error,
|
||||
});
|
||||
|
||||
WorkspaceState copyWith({
|
||||
List<Workspace>? workspaces,
|
||||
Workspace? selectedWorkspace,
|
||||
List<WorkspaceMember>? members,
|
||||
bool? isLoading,
|
||||
String? error,
|
||||
}) =>
|
||||
WorkspaceState(
|
||||
workspaces: workspaces ?? this.workspaces,
|
||||
selectedWorkspace: selectedWorkspace ?? this.selectedWorkspace,
|
||||
members: members ?? this.members,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
error: error,
|
||||
);
|
||||
}
|
||||
|
||||
class WorkspaceNotifier extends StateNotifier<WorkspaceState> {
|
||||
final ApiClient _api;
|
||||
|
||||
WorkspaceNotifier(this._api) : super(WorkspaceState());
|
||||
|
||||
Future<void> loadWorkspaces() async {
|
||||
state = state.copyWith(isLoading: true, error: null);
|
||||
try {
|
||||
final response = await _api.get(Endpoints.workspaces);
|
||||
final list = (response.data as List<dynamic>)
|
||||
.map((e) => Workspace.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
state = state.copyWith(workspaces: list, isLoading: false);
|
||||
} on ApiException catch (e) {
|
||||
state = state.copyWith(isLoading: false, error: e.message);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Workspace?> createWorkspace(CreateWorkspaceRequest request) async {
|
||||
try {
|
||||
final response =
|
||||
await _api.post(Endpoints.workspaces, data: request.toJson());
|
||||
final ws =
|
||||
Workspace.fromJson(response.data as Map<String, dynamic>);
|
||||
state = state.copyWith(workspaces: [...state.workspaces, ws]);
|
||||
return ws;
|
||||
} on ApiException catch (e) {
|
||||
state = state.copyWith(error: e.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> selectWorkspace(Workspace workspace) async {
|
||||
state = state.copyWith(selectedWorkspace: workspace);
|
||||
await loadMembers(workspace.id);
|
||||
}
|
||||
|
||||
Future<void> loadMembers(String workspaceId) async {
|
||||
try {
|
||||
final response =
|
||||
await _api.get(Endpoints.workspaceMembers(workspaceId));
|
||||
final list = (response.data as List<dynamic>)
|
||||
.map((e) => WorkspaceMember.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
state = state.copyWith(members: list);
|
||||
} on ApiException catch (e) {
|
||||
state = state.copyWith(error: e.message);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> addMember(String workspaceId, String email, String role) async {
|
||||
try {
|
||||
await _api.post(Endpoints.workspaceMembers(workspaceId), data: {
|
||||
'email': email,
|
||||
'role': role,
|
||||
});
|
||||
await loadMembers(workspaceId);
|
||||
return true;
|
||||
} on ApiException catch (e) {
|
||||
state = state.copyWith(error: e.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> removeMember(String workspaceId, String userId) async {
|
||||
try {
|
||||
await _api.delete(Endpoints.workspaceMember(workspaceId, userId));
|
||||
state = state.copyWith(
|
||||
members: state.members.where((m) => m.userId != userId).toList(),
|
||||
);
|
||||
return true;
|
||||
} on ApiException catch (e) {
|
||||
state = state.copyWith(error: e.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void clearError() {
|
||||
state = state.copyWith(error: null);
|
||||
}
|
||||
}
|
||||
|
||||
final workspaceProvider =
|
||||
StateNotifierProvider<WorkspaceNotifier, WorkspaceState>((ref) {
|
||||
final api = ref.read(apiClientProvider);
|
||||
return WorkspaceNotifier(api);
|
||||
});
|
||||
Reference in New Issue
Block a user