Skip to content

Commit d7884da

Browse files
committed
the app :)
1 parent e0fbf95 commit d7884da

21 files changed

+1538
-0
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import 'dart:convert';
2+
import 'package:http/http.dart' as http;
3+
import 'package:logging/logging.dart';
4+
5+
final log = Logger('powersync-django-todolist');
6+
7+
class ApiClient {
8+
final String baseUrl;
9+
10+
ApiClient(this.baseUrl);
11+
12+
Future<Map<String, dynamic>> authenticate(String username, String password) async {
13+
final response = await http.post(
14+
Uri.parse('$baseUrl/api/auth/'),
15+
headers: {'Content-Type': 'application/json'},
16+
body: json.encode({'username': username, 'password': password}),
17+
);
18+
if (response.statusCode == 200) {
19+
return json.decode(response.body);
20+
} else {
21+
throw Exception('Failed to authenticate');
22+
}
23+
}
24+
25+
Future<Map<String, dynamic>> getToken(String userId) async {
26+
final response = await http.get(
27+
Uri.parse('$baseUrl/api/get_powersync_token/'),
28+
headers: {'Content-Type': 'application/json'},
29+
);
30+
if (response.statusCode == 200) {
31+
return json.decode(response.body);
32+
} else {
33+
throw Exception('Failed to fetch token');
34+
}
35+
}
36+
37+
Future<void> upsert(Map<String, dynamic> record) async {
38+
await http.put(
39+
Uri.parse('$baseUrl/api/upload_data/'),
40+
headers: {'Content-Type': 'application/json'},
41+
body: json.encode(record),
42+
);
43+
}
44+
45+
Future<void> update(Map<String, dynamic> record) async {
46+
await http.patch(
47+
Uri.parse('$baseUrl/api/upload_data/'),
48+
headers: {'Content-Type': 'application/json'},
49+
body: json.encode(record),
50+
);
51+
}
52+
53+
Future<void> delete(Map<String, dynamic> record) async {
54+
await http.delete(
55+
Uri.parse('$baseUrl/api/upload_data/'),
56+
headers: {'Content-Type': 'application/json'},
57+
body: json.encode(record),
58+
);
59+
}
60+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Copy this template: `cp lib/app_config_template.dart lib/app_config.dart`
2+
// Edit lib/app_config.dart and enter your Django and PowerSync project details.
3+
class AppConfig {
4+
static const String djangoUrl = 'https://foo.ngrok.app';
5+
static const String powersyncUrl = 'https://myprojectid.powersync.journeyapps.com';
6+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import 'package:powersync_flutter_demo/powersync.dart';
2+
3+
String _createSearchTermWithOptions(String searchTerm) {
4+
// adding * to the end of the search term will match any word that starts with the search term
5+
// e.g. searching bl will match blue, black, etc.
6+
// consult FTS5 Full-text Query Syntax documentation for more options
7+
String searchTermWithOptions = '$searchTerm*';
8+
return searchTermWithOptions;
9+
}
10+
11+
/// Search the FTS table for the given searchTerm
12+
Future<List> search(String searchTerm, String tableName) async {
13+
String searchTermWithOptions = _createSearchTermWithOptions(searchTerm);
14+
return await db.getAll(
15+
'SELECT * FROM fts_$tableName WHERE fts_$tableName MATCH ? ORDER BY rank',
16+
[searchTermWithOptions]);
17+
}

demos/django-todolist/lib/main.dart

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import 'package:flutter/foundation.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:logging/logging.dart';
4+
import 'package:powersync_flutter_demo/models/schema.dart';
5+
6+
import './powersync.dart';
7+
import './widgets/lists_page.dart';
8+
import './widgets/login_page.dart';
9+
import './widgets/query_widget.dart';
10+
import './widgets/status_app_bar.dart';
11+
12+
13+
void main() async {
14+
Logger.root.level = Level.INFO;
15+
Logger.root.onRecord.listen((record) {
16+
if (kDebugMode) {
17+
print(
18+
'[${record.loggerName}] ${record.level.name}: ${record.time}: ${record.message}');
19+
20+
if (record.error != null) {
21+
print(record.error);
22+
}
23+
if (record.stackTrace != null) {
24+
print(record.stackTrace);
25+
}
26+
}
27+
});
28+
29+
WidgetsFlutterBinding
30+
.ensureInitialized(); //required to get sqlite filepath from path_provider before UI has initialized
31+
32+
await openDatabase();
33+
34+
final loggedIn = await isLoggedIn();
35+
36+
runApp(MyApp(loggedIn: loggedIn));
37+
}
38+
39+
const defaultQuery = 'SELECT * from $todosTable';
40+
41+
const listsPage = ListsPage();
42+
const homePage = listsPage;
43+
44+
const sqlConsolePage = Scaffold(
45+
appBar: StatusAppBar(title: 'SQL Console'),
46+
body: QueryWidget(defaultQuery: defaultQuery));
47+
48+
const loginPage = LoginPage();
49+
50+
class MyApp extends StatelessWidget {
51+
final bool loggedIn;
52+
53+
const MyApp({super.key, required this.loggedIn});
54+
55+
@override
56+
Widget build(BuildContext context) {
57+
return MaterialApp(
58+
title: 'PowerSync Django Todolist Demo',
59+
theme: ThemeData(
60+
primarySwatch: Colors.blue,
61+
),
62+
home: loggedIn ? homePage : loginPage);
63+
}
64+
}
65+
66+
class MyHomePage extends StatelessWidget {
67+
const MyHomePage(
68+
{super.key,
69+
required this.title,
70+
required this.content,
71+
this.floatingActionButton});
72+
73+
final String title;
74+
final Widget content;
75+
final Widget? floatingActionButton;
76+
77+
@override
78+
Widget build(BuildContext context) {
79+
return Scaffold(
80+
appBar: StatusAppBar(title: title),
81+
body: Center(child: content),
82+
floatingActionButton: floatingActionButton,
83+
drawer: Drawer(
84+
// Add a ListView to the drawer. This ensures the user can scroll
85+
// through the options in the drawer if there isn't enough vertical
86+
// space to fit everything.
87+
child: ListView(
88+
// Important: Remove any padding from the ListView.
89+
padding: EdgeInsets.zero,
90+
children: [
91+
const DrawerHeader(
92+
decoration: BoxDecoration(
93+
color: Colors.blue,
94+
),
95+
child: Text(''),
96+
),
97+
ListTile(
98+
title: const Text('SQL Console'),
99+
onTap: () {
100+
var navigator = Navigator.of(context);
101+
navigator.pop();
102+
103+
navigator.push(MaterialPageRoute(
104+
builder: (context) => sqlConsolePage,
105+
));
106+
},
107+
),
108+
ListTile(
109+
title: const Text('Sign Out'),
110+
onTap: () async {
111+
var navigator = Navigator.of(context);
112+
navigator.pop();
113+
await logout();
114+
115+
navigator.pushReplacement(MaterialPageRoute(
116+
builder: (context) => loginPage,
117+
));
118+
},
119+
),
120+
],
121+
),
122+
),
123+
);
124+
}
125+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import 'package:powersync/powersync.dart';
2+
import 'package:sqlite_async/sqlite_async.dart';
3+
4+
import 'helpers.dart';
5+
import '../models/schema.dart';
6+
7+
final migrations = SqliteMigrations();
8+
9+
/// Create a Full Text Search table for the given table and columns
10+
/// with an option to use a different tokenizer otherwise it defaults
11+
/// to unicode61. It also creates the triggers that keep the FTS table
12+
/// and the PowerSync table in sync.
13+
SqliteMigration createFtsMigration(
14+
{required int migrationVersion,
15+
required String tableName,
16+
required List<String> columns,
17+
String tokenizationMethod = 'unicode61'}) {
18+
String internalName =
19+
schema.tables.firstWhere((table) => table.name == tableName).internalName;
20+
String stringColumns = columns.join(', ');
21+
22+
return SqliteMigration(migrationVersion, (tx) async {
23+
// Add FTS table
24+
await tx.execute('''
25+
CREATE VIRTUAL TABLE IF NOT EXISTS fts_$tableName
26+
USING fts5(id UNINDEXED, $stringColumns, tokenize='$tokenizationMethod');
27+
''');
28+
// Copy over records already in table
29+
await tx.execute('''
30+
INSERT INTO fts_$tableName(rowid, id, $stringColumns)
31+
SELECT rowid, id, ${generateJsonExtracts(ExtractType.columnOnly, 'data', columns)} FROM $internalName;
32+
''');
33+
// Add INSERT, UPDATE and DELETE and triggers to keep fts table in sync with table
34+
await tx.execute('''
35+
CREATE TRIGGER IF NOT EXISTS fts_insert_trigger_$tableName AFTER INSERT ON $internalName
36+
BEGIN
37+
INSERT INTO fts_$tableName(rowid, id, $stringColumns)
38+
VALUES (
39+
NEW.rowid,
40+
NEW.id,
41+
${generateJsonExtracts(ExtractType.columnOnly, 'NEW.data', columns)}
42+
);
43+
END;
44+
''');
45+
await tx.execute('''
46+
CREATE TRIGGER IF NOT EXISTS fts_update_trigger_$tableName AFTER UPDATE ON $internalName BEGIN
47+
UPDATE fts_$tableName
48+
SET ${generateJsonExtracts(ExtractType.columnInOperation, 'NEW.data', columns)}
49+
WHERE rowid = NEW.rowid;
50+
END;
51+
''');
52+
await tx.execute('''
53+
CREATE TRIGGER IF NOT EXISTS fts_delete_trigger_$tableName AFTER DELETE ON $internalName BEGIN
54+
DELETE FROM fts_$tableName WHERE rowid = OLD.rowid;
55+
END;
56+
''');
57+
});
58+
}
59+
60+
/// This is where you can add more migrations to generate FTS tables
61+
/// that correspond to the tables in your schema and populate them
62+
/// with the data you would like to search on
63+
Future<void> configureFts(PowerSyncDatabase db) async {
64+
migrations
65+
..add(createFtsMigration(
66+
migrationVersion: 1,
67+
tableName: 'lists',
68+
columns: ['name'],
69+
tokenizationMethod: 'porter unicode61'))
70+
..add(createFtsMigration(
71+
migrationVersion: 2,
72+
tableName: 'todos',
73+
columns: ['description', 'list_id'],
74+
));
75+
await migrations.migrate(db);
76+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
typedef ExtractGenerator = String Function(String, String);
2+
3+
enum ExtractType {
4+
columnOnly,
5+
columnInOperation,
6+
}
7+
8+
typedef ExtractGeneratorMap = Map<ExtractType, ExtractGenerator>;
9+
10+
String _createExtract(String jsonColumnName, String columnName) =>
11+
'json_extract($jsonColumnName, \'\$.$columnName\')';
12+
13+
ExtractGeneratorMap extractGeneratorsMap = {
14+
ExtractType.columnOnly: (
15+
String jsonColumnName,
16+
String columnName,
17+
) =>
18+
_createExtract(jsonColumnName, columnName),
19+
ExtractType.columnInOperation: (
20+
String jsonColumnName,
21+
String columnName,
22+
) =>
23+
'$columnName = ${_createExtract(jsonColumnName, columnName)}',
24+
};
25+
26+
String generateJsonExtracts(
27+
ExtractType type, String jsonColumnName, List<String> columns) {
28+
ExtractGenerator? generator = extractGeneratorsMap[type];
29+
if (generator == null) {
30+
throw StateError('Unexpected null generator for key: $type');
31+
}
32+
33+
if (columns.length == 1) {
34+
return generator(jsonColumnName, columns.first);
35+
}
36+
37+
return columns.map((column) => generator(jsonColumnName, column)).join(', ');
38+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import 'package:powersync/powersync.dart';
2+
3+
const todosTable = 'todos';
4+
5+
Schema schema = const Schema(([
6+
Table(todosTable, [
7+
Column.text('list_id'),
8+
Column.text('created_at'),
9+
Column.text('completed_at'),
10+
Column.text('description'),
11+
Column.integer('completed'),
12+
Column.text('created_by'),
13+
Column.text('completed_by'),
14+
], indexes: [
15+
// Index to allow efficient lookup within a list
16+
Index('list', [IndexedColumn('list_id')])
17+
]),
18+
Table('lists', [
19+
Column.text('created_at'),
20+
Column.text('name'),
21+
Column.text('owner_id')
22+
])
23+
]));

0 commit comments

Comments
 (0)