Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NocoDB Cloud support #22

Merged
merged 3 commits into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
run_integration_test rit:
flutter run --dart-define-from-file=integration_test/.env -t integration_test/hello_test.dart

fmt:
fix:
dart fix --apply lib
dart fix --apply integration_test
dart format lib integration_test
Expand Down
6 changes: 6 additions & 0 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ PODS:
- Flutter
- image_picker_ios (0.0.1):
- Flutter
- integration_test (0.0.1):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
Expand All @@ -60,6 +62,7 @@ DEPENDENCIES:
- flutter_downloader (from `.symlinks/plugins/flutter_downloader/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
Expand All @@ -82,6 +85,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
integration_test:
:path: ".symlinks/plugins/integration_test/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
shared_preferences_foundation:
Expand All @@ -97,6 +102,7 @@ SPEC CHECKSUMS:
flutter_downloader: b7301ae057deadd4b1650dc7c05375f10ff12c39
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
integration_test: 13825b8a9334a850581300559b8839134b124670
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
SDWebImage: dfe95b2466a9823cf9f0c6d01217c06550d7b29a
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
Expand Down
88 changes: 56 additions & 32 deletions lib/common/settings.dart
Original file line number Diff line number Diff line change
@@ -1,52 +1,76 @@
import 'dart:io';

import 'package:flutter/foundation.dart';

import 'package:nocodb/common/preferences.dart';

final settings = Settings();
import 'package:nocodb/nocodb_sdk/client.dart';

final settings = _Settings();

class Settings {
Settings({required this.host, required this.token, this.username});

final String? username;
final String host;
final Token token;
}

const _kUsername = 'username';
const _kHost = 'host';
const _kAuthToken = 'auth_token';
const _kApiToken = 'api_token';

class _Settings {
Preferences? prefs;
bool get initialized => prefs != null;
init(Preferences prefs) {
this.prefs = prefs;
}

static const _authToken = 'authToken';
Future<String?> get authToken async =>
await prefs?.getSecure(key: _authToken);
Future<void> save({
required String host,
required Token token,
String? username,
}) async {
await clear();
await prefs?.set(key: _kHost, value: host);
if (username != null) {
await prefs?.set(key: _kUsername, value: username);
}

Future<void> setAuthToken(String v) async =>
await prefs?.set(key: _authToken, value: v, secure: true);
switch (token) {
case AuthToken(authToken: final authToken):
await prefs?.set(key: _kAuthToken, value: authToken, secure: true);
case ApiToken(apiToken: final apiToken):
await prefs?.set(key: _kApiToken, value: apiToken, secure: true);
default:
throw Exception('unsupported token type: ${token.runtimeType}');
}
}

static const _email = 'email';
Future<String?> get email async => await prefs?.get(key: _email);
Future<Settings?> get() async {
final host = await prefs?.get<String>(key: _kHost);
if (host == null) {
return null;
}

Future<void> setEmail(String v) async => await prefs?.set(
key: _email,
value: v,
final username = await prefs?.get<String>(key: _kUsername);
final authToken = await prefs?.getSecure(key: _kAuthToken);
if (authToken != null) {
return Settings(
username: username,
host: host,
token: AuthToken(authToken),
);

static const _apiBaseUrl = 'apiBaseUrl';
Future<String?> get apiBaseUrl async {
final v = await prefs?.get<String>(key: _apiBaseUrl);
if (v == null && !kIsWeb && Platform.isAndroid) {
return 'http://10.0.2.2:8080';
}
return v;
}

Future<void> setApiBaseUrl(String v) async =>
await prefs?.set(key: _apiBaseUrl, value: v);

static const _rememberMe = 'rememberMe';
Future<bool> get rememberMe async =>
await prefs?.get<bool>(key: _rememberMe) ?? false;
Future<void> setRememberMe(bool v) async => await prefs?.set(
key: _rememberMe,
value: v,
final apiToken = await prefs?.getSecure(key: _kApiToken);
if (apiToken != null) {
return Settings(
username: username,
host: host,
token: ApiToken(apiToken),
);
}
return null;
}

Future<void> clear() async {
await prefs?.clear();
Expand Down
4 changes: 2 additions & 2 deletions lib/features/core/components/attachment_image_card.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:nocodb/nocodb_sdk/client.dart';
import 'package:nocodb/nocodb_sdk/models.dart';
import 'package:popup_menu/popup_menu.dart';

Expand All @@ -21,7 +20,8 @@ class AttachmentImageCard extends HookConsumerWidget {
width: 80,
height: 80,
child: CachedNetworkImage(
imageUrl: _file.signedUrl(api.uri),
// imageUrl: _file.getFullUrl(api.uri),
imageUrl: _file.signedUrl,
placeholder: (context, url) => const Padding(
padding: EdgeInsets.all(24),
child: CircularProgressIndicator(),
Expand Down
4 changes: 2 additions & 2 deletions lib/features/core/components/cells/attachment.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

import 'package:nocodb/nocodb_sdk/client.dart';
import 'package:nocodb/nocodb_sdk/models.dart';

class Attachment extends HookConsumerWidget {
Expand All @@ -15,7 +14,8 @@ class Attachment extends HookConsumerWidget {
? Padding(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 2),
child: CachedNetworkImage(
imageUrl: file.signedUrl(api.uri),
// imageUrl: file.getFullUrl(api.uri),
imageUrl: file.signedUrl,
placeholder: (context, url) =>
const CircularProgressIndicator(),
errorWidget: (context, url, error) => const Icon(Icons.error),
Expand Down
3 changes: 2 additions & 1 deletion lib/features/core/pages/attachment_editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,8 @@ class AttachmentEditorPage extends HookConsumerWidget {
}

await FlutterDownloader.enqueue(
url: file.signedUrl(api.uri),
// url: file.getFullUrl(api.uri),
url: file.signedUrl,
fileName: file.title,
savedDir: downloadDir,
showNotification: true,
Expand Down
135 changes: 135 additions & 0 deletions lib/features/core/pages/cloud_project_list.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:nocodb/common/settings.dart';
import 'package:nocodb/features/core/components/dialog/new_project_dialog.dart';
import 'package:nocodb/features/core/providers/providers.dart';
import 'package:nocodb/nocodb_sdk/models.dart';
import 'package:nocodb/routes.dart';

const _divider = Divider(height: 1);

class _ProjectList extends HookConsumerWidget {
const _ProjectList();

@override
Widget build(BuildContext context, WidgetRef ref) {
final workspace = ref.watch(workspaceProvider);
return ref.watch(baseListProvider(workspace!.id)).when(
data: (data) => _build(ref, data),
error: (e, s) => Text('$e\n$s'),
loading: () => const Center(child: CircularProgressIndicator()),
);
}

Widget _build(WidgetRef ref, NcProjectList projectList) {
final context = useContext();
// TODO: Pagination
final content = Flexible(
child: ListView.separated(
shrinkWrap: true,
itemCount: projectList.list.length,
itemBuilder: (context, index) {
final project = projectList.list[index];
return Container(
margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 4),
child: ListTile(
title: Text(project.title),
onTap: () async {
ref.read(projectProvider.notifier).state = project;
await const SheetRoute().push(context);
},
),
);
},
separatorBuilder: (context, index) => _divider,
),
);

return Column(
children: [
content,
_divider,
Container(
margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 4),
child: ListTile(
title: const Text('New Project'),
onTap: () async {
await showDialog(
context: context,
builder: (_) => const NewProjectDialog(),
);
},
),
),
_divider,
],
);
}
}

class CloudProjectListPage extends HookConsumerWidget {
const CloudProjectListPage({super.key});

Widget _buildScaffold(
WidgetRef ref,
Widget body,
NcList<NcWorkspace> workspaceList,
) {
final workspace = ref.watch(workspaceProvider);
final context = useContext();
return Scaffold(
appBar: AppBar(
title: DropdownButton<NcWorkspace>(
items: workspaceList.list
.map(
(e) => DropdownMenuItem(
value: e,
child: Text('${e.title} (${e.id})'),
),
)
.toList(),
onChanged: (value) {
if (value != null) {
ref.read(workspaceProvider.notifier).state = value;
}
},
value: workspace,
),
actions: [
PopupMenuButton(
icon: const Icon(Icons.account_circle),
itemBuilder: (context) => <PopupMenuEntry>[
PopupMenuItem(
child: const ListTile(
title: Text('Logout'),
),
onTap: () async {
await settings
.clear()
.then((value) => const HomeRoute().push(context));
},
),
],
),
const PopupMenuDivider(),
IconButton(
onPressed: () async {
await const DebugRoute().push(context);
},
icon: const Icon(Icons.bug_report),
),
],
),
body: body,
);
}

@override
Widget build(BuildContext context, WidgetRef ref) =>
ref.watch(workspaceListProvider).when(
data: (data) => _buildScaffold(ref, const _ProjectList(), data),
error: (e, s) => Center(child: Text('$e\n$s')),
loading: () => const Center(child: CircularProgressIndicator()),
);
}
19 changes: 18 additions & 1 deletion lib/features/core/providers/providers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'providers.g.dart';

final workspaceProvider = StateProvider<NcWorkspace?>((ref) => null);
final projectProvider = StateProvider<NcProject?>((ref) => null);

final tableProvider = StateProvider<NcTable?>((ref) => null);
Expand Down Expand Up @@ -97,7 +98,23 @@ FutureOr<T2> _unwrap2<T1, T2>(
);

@riverpod
Future<NcList<NcProject>> projectList(ProjectListRef ref) async =>
Future<NcWorkspaceList> workspaceList(WorkspaceListRef ref) async =>
(await api.workspaceList()).when(
ok: (ok) {
if (ref.read(workspaceProvider) == null) {
ref.read(workspaceProvider.notifier).state = ok.list.firstOrNull;
}
return ok;
},
ng: _errorAdapter,
);

@riverpod
Future<NcProjectList> baseList(BaseListRef ref, workspaceId) async =>
_unwrap(await api.baseList(workspaceId));

@riverpod
Future<NcProjectList> projectList(ProjectListRef ref) async =>
_unwrap(await api.projectList());

@Riverpod(keepAlive: true)
Expand Down
Loading