Skip to content

Commit dce6167

Browse files
authored
Initial web support (#204)
2 parents 9c46f68 + 12023d7 commit dce6167

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1141
-232
lines changed

.github/workflows/build.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,3 +349,28 @@ jobs:
349349
run: |
350350
flutterpi_tool build --release --cpu=pi4
351351
352+
build_web:
353+
name: Bluecherry Web
354+
runs-on: ubuntu-latest
355+
steps:
356+
- name: Checkout
357+
uses: actions/checkout@v3
358+
with:
359+
token: ${{ secrets.GITHUB_TOKEN }}
360+
submodules: recursive
361+
362+
- name: Install Flutter
363+
uses: subosito/flutter-action@v2.8.0
364+
with:
365+
channel: "stable"
366+
cache: false
367+
368+
- name: Initiate Flutter
369+
run: |
370+
flutter gen-l10n
371+
flutter pub get
372+
373+
- name: Build
374+
run: |
375+
flutter build web --verbose --dart-define=FLUTTER_WEB_USE_SKIA=true --dart-define=FLUTTER_WEB_AUTO_DETECT=true
376+

.gitignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
.fvm/
3434

3535
# Web related
36-
lib/generated_plugin_registrant.dart
3736

3837
# Symbolication related
3938
app.*.symbols
@@ -66,4 +65,4 @@ AppDirassets/
6665
*.tar.gz
6766
rpmbuild/
6867

69-
bluecherry_config
68+
bluecherry_config

.metadata

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,20 @@
44
# This file should be version controlled and should not be manually edited.
55

66
version:
7-
revision: "9e1c857886f07d342cf106f2cd588bcd5e031bb2"
8-
channel: "stable"
7+
revision: "984dc1947b574a51d5493e9c3b866a8218c69192"
8+
channel: "master"
99

1010
project_type: app
1111

1212
# Tracks metadata for the flutter migrate command
1313
migration:
1414
platforms:
1515
- platform: root
16-
create_revision: 9e1c857886f07d342cf106f2cd588bcd5e031bb2
17-
base_revision: 9e1c857886f07d342cf106f2cd588bcd5e031bb2
18-
- platform: macos
19-
create_revision: 9e1c857886f07d342cf106f2cd588bcd5e031bb2
20-
base_revision: 9e1c857886f07d342cf106f2cd588bcd5e031bb2
16+
create_revision: 984dc1947b574a51d5493e9c3b866a8218c69192
17+
base_revision: 984dc1947b574a51d5493e9c3b866a8218c69192
18+
- platform: web
19+
create_revision: 984dc1947b574a51d5493e9c3b866a8218c69192
20+
base_revision: 984dc1947b574a51d5493e9c3b866a8218c69192
2121

2222
# User provided section
2323

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,18 @@ flutter build [linux|windows|macos|android|ios]
166166

167167
The automated build process is done using GitHub Actions. You may find the workflow [here](.github/workflows/main.yml). The workflow builds the app for all supported platforms & uploads the artifacts to the release page.
168168

169+
#### Linux
170+
169171
On Linux, a Flutter executable with different environment variables is used to build the app for different distributions. This tells the app how the system is configured and how it should install updates. To run for Linux, you need to provide the following environment variables based on your system, where `[DISTRO_ENV]` can be `appimage` (AppImage), `deb` (Debian), `rpm` (RedHat), `tar.gz` (Tarball) or `pi` (Raspberry Pi).
170172

171173
```bash
172-
flutter run --dart-define-from-file=linux/env/[DISTRO_ENV].json
174+
flutter run -d linux --dart-define-from-file=linux/env/[DISTRO_ENV].json
173175
```
176+
177+
#### Web
178+
179+
When running on debug, you must disable the CORS policy in your browser. Note that this is only for debugging purposes and should not be used in production. To do this, run the following command:
180+
181+
```bash
182+
flutter run -d chrome --web-browser-flag "--disable-web-security"
183+
```

lib/api/api.dart

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import 'package:bluecherry_client/api/api_helpers.dart';
2323
import 'package:bluecherry_client/models/device.dart';
2424
import 'package:bluecherry_client/models/server.dart';
2525
import 'package:flutter/foundation.dart';
26-
import 'package:http/http.dart';
26+
import 'package:http/http.dart' as http;
2727
import 'package:xml2json/xml2json.dart';
2828

2929
export 'events.dart';
@@ -32,6 +32,19 @@ export 'ptz.dart';
3232
class API {
3333
static final API instance = API();
3434

35+
static final client = http.Client();
36+
37+
static void initialize() {
38+
if (kIsWeb) {
39+
// On Web, a [BrowserClient] is used under the hood, which has the
40+
// "withCredentials" property. This is cast as dynamic because the
41+
// [BrowserClient] is not available on the other platforms.
42+
//
43+
// This is used to enable the cookies on the requests.
44+
(client as dynamic).withCredentials = true;
45+
}
46+
}
47+
3548
/// Checks details of a [server] entered by the user.
3649
/// If the attributes present in [Server] are correct, then the
3750
/// returned object will have [Server.serverUUID] & [Server.cookie]
@@ -45,10 +58,10 @@ class API {
4558
{
4659
'login': server.login,
4760
'password': server.password,
48-
'from_client': 'true',
61+
'from_client': '${true}',
4962
},
5063
);
51-
final request = MultipartRequest('POST', uri)
64+
final request = http.MultipartRequest('POST', uri)
5265
..fields.addAll({
5366
'login': server.login,
5467
'password': server.password,
@@ -59,14 +72,18 @@ class API {
5972
});
6073
final response = await request.send();
6174
final body = await response.stream.bytesToString();
62-
debugPrint('checkServerCredentials ${response.statusCode}');
63-
// debugPrint(response.headers.toString());
75+
debugPrint(
76+
'checkServerCredentials ${response.statusCode}'
77+
'\n:....${response.headers}'
78+
'\n:....$body',
79+
);
6480

6581
if (response.statusCode == 200) {
6682
final json = await compute(jsonDecode, body);
6783
return server.copyWith(
6884
serverUUID: json['server_uuid'],
69-
cookie: response.headers['set-cookie'],
85+
cookie:
86+
response.headers['set-cookie'] ?? response.headers['Set-Cookie'],
7087
online: true,
7188
);
7289
} else {
@@ -93,8 +110,8 @@ class API {
93110
}
94111

95112
try {
96-
assert(server.serverUUID != null && server.cookie != null);
97-
final response = await get(
113+
assert(server.serverUUID != null && server.hasCookies);
114+
final response = await client.get(
98115
Uri.https(
99116
'${Uri.encodeComponent(server.login)}:${Uri.encodeComponent(server.password)}@${server.ip}:${server.port}',
100117
'/devices.php',
@@ -144,8 +161,8 @@ class API {
144161
///
145162
Future<String?> getNotificationAPIEndpoint(Server server) async {
146163
try {
147-
assert(server.serverUUID != null && server.cookie != null);
148-
final response = await get(
164+
assert(server.serverUUID != null && server.hasCookies);
165+
final response = await client.get(
149166
Uri.https(
150167
'${Uri.encodeComponent(server.login)}:${Uri.encodeComponent(server.password)}@${server.ip}:${server.port}',
151168
'/mobile-app-config.json',
@@ -174,7 +191,7 @@ class API {
174191
assert(uri != null, '[getNotificationAPIEndpoint] returned null.');
175192
assert(clientID != null, '[clientUUID] returned null.');
176193
assert(server.serverUUID != null, '[server.serverUUID] is null.');
177-
final response = await post(
194+
final response = await client.post(
178195
Uri.parse('${uri!}store-token'),
179196
headers: {
180197
'Cookie': server.cookie!,
@@ -216,7 +233,7 @@ class API {
216233
assert(uri != null, '[getNotificationAPIEndpoint] returned null.');
217234
assert(clientID != null, '[clientUUID] returned null.');
218235
assert(server.serverUUID != null, '[server.serverUUID] is null.');
219-
final response = await post(
236+
final response = await client.post(
220237
Uri.parse('${uri!}remove-token'),
221238
headers: {
222239
'Cookie': server.cookie!,

lib/api/api_helpers.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import 'package:bluecherry_client/models/server.dart';
2424
import 'package:bluecherry_client/providers/server_provider.dart';
2525
import 'package:device_info_plus/device_info_plus.dart';
2626
import 'package:flutter/foundation.dart';
27-
import 'package:http/http.dart';
2827
import 'package:path_provider/path_provider.dart';
2928

3029
/// This file mainly contains helper functions for working with the API.
@@ -110,7 +109,7 @@ abstract class APIHelpers {
110109
return 'file://$filePath';
111110
// Download the event thumbnail only if it doesn't exist already.
112111
} else {
113-
final response = await get(uri);
112+
final response = await API.client.get(uri);
114113
debugPrint(response.statusCode.toString());
115114
if (response.statusCode ~/ 100 == 2 /* OK */) {
116115
await file.create(recursive: true);

lib/api/events.dart

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import 'package:bluecherry_client/models/event.dart';
77
import 'package:bluecherry_client/models/server.dart';
88
import 'package:bluecherry_client/utils/methods.dart';
99
import 'package:flutter/foundation.dart';
10-
import 'package:http/http.dart' as http;
1110
import 'package:xml2json/xml2json.dart';
1211

1312
extension EventsExtension on API {
@@ -70,8 +69,8 @@ extension EventsExtension on API {
7069
'${deviceId != null ? 'for device $deviceId' : ''}',
7170
);
7271

73-
assert(server.serverUUID != null && server.cookie != null);
74-
final response = await http.get(
72+
assert(server.serverUUID != null && server.hasCookies);
73+
final response = await API.client.get(
7574
Uri.https(
7675
'${Uri.encodeComponent(server.login)}:${Uri.encodeComponent(server.password)}@${server.ip}:${server.port}',
7776
'/events/',

lib/api/ptz.dart

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import 'package:bluecherry_client/api/api.dart';
2121
import 'package:bluecherry_client/models/device.dart';
2222
import 'package:flutter/widgets.dart';
2323
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
24-
import 'package:http/http.dart' as http;
2524

2625
enum PTZCommand {
2726
move,
@@ -113,7 +112,7 @@ extension PtzApiExtension on API {
113112

114113
debugPrint(url.toString());
115114

116-
final response = await http.get(
115+
final response = await API.client.get(
117116
url,
118117
headers: {
119118
'Content-Type': 'application/x-www-form-urlencoded',
@@ -156,7 +155,7 @@ extension PtzApiExtension on API {
156155

157156
debugPrint(url.toString());
158157

159-
final response = await http.get(
158+
final response = await API.client.get(
160159
url,
161160
headers: {
162161
'Content-Type': 'application/x-www-form-urlencoded',

lib/firebase_messaging_background_handler.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@ abstract class FirebaseConfiguration {
445445
FirebaseMessaging.instance.getToken().then((token) async {
446446
debugPrint('[FirebaseMessaging.instance.getToken]: $token');
447447
if (token != null) {
448-
final data = await storage.read() as Map;
448+
final data = await tryReadStorage(() => storage.read());
449449
// Do not proceed, if token is already saved.
450450
if (data[kHiveNotificationToken] == token) {
451451
return;

lib/main.dart

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import 'dart:async';
2121
import 'dart:convert';
2222
import 'dart:io';
2323

24+
import 'package:bluecherry_client/api/api.dart';
2425
import 'package:bluecherry_client/api/api_helpers.dart';
2526
import 'package:bluecherry_client/firebase_messaging_background_handler.dart';
2627
import 'package:bluecherry_client/models/device.dart';
@@ -35,7 +36,7 @@ import 'package:bluecherry_client/providers/mobile_view_provider.dart';
3536
import 'package:bluecherry_client/providers/server_provider.dart';
3637
import 'package:bluecherry_client/providers/settings_provider.dart';
3738
import 'package:bluecherry_client/providers/update_provider.dart';
38-
import 'package:bluecherry_client/utils/app_links.dart' as app_links;
39+
import 'package:bluecherry_client/utils/app_links/app_links.dart' as app_links;
3940
import 'package:bluecherry_client/utils/logging.dart' as logging;
4041
import 'package:bluecherry_client/utils/methods.dart';
4142
import 'package:bluecherry_client/utils/storage.dart';
@@ -81,6 +82,7 @@ Future<void> main(List<String> args) async {
8182
}
8283

8384
DevHttpOverrides.configureCertificates();
85+
API.initialize();
8486
await UnityVideoPlayerInterface.instance.initialize();
8587
if (isDesktopPlatform && Platform.isLinux) {
8688
if (UpdateManager.linuxEnvironment == LinuxPlatform.embedded) {
@@ -146,7 +148,7 @@ Future<void> main(List<String> args) async {
146148
// Request notifications permission for iOS, Android 13+ and Windows.
147149
//
148150
// permission_handler only supports these platforms
149-
if (isMobilePlatform || Platform.isWindows) {
151+
if (kIsWeb || isMobilePlatform || Platform.isWindows) {
150152
() async {
151153
if (await Permission.notification.isDenied) {
152154
final state = await Permission.notification.request();
@@ -170,8 +172,8 @@ Future<void> main(List<String> args) async {
170172
UpdateManager.ensureInitialized(),
171173
]);
172174

173-
/// Firebase messaging isn't available on Windows nor Linux
174-
if (kIsWeb || isMobilePlatform || Platform.isMacOS) {
175+
/// Firebase messaging isn't available on windows nor linux
176+
if (!kIsWeb && (isMobilePlatform || Platform.isMacOS)) {
175177
FirebaseConfiguration.ensureInitialized();
176178
}
177179

@@ -198,8 +200,8 @@ class _UnityAppState extends State<UnityApp>
198200
void initState() {
199201
super.initState();
200202
WidgetsBinding.instance.addObserver(this);
201-
windowManager.addListener(this);
202203
if (isDesktopPlatform && canConfigureWindow) {
204+
windowManager.addListener(this);
203205
windowManager.setPreventClose(true).then((_) {
204206
if (mounted) setState(() {});
205207
});
@@ -209,7 +211,9 @@ class _UnityAppState extends State<UnityApp>
209211
@override
210212
void dispose() {
211213
WidgetsBinding.instance.removeObserver(this);
212-
windowManager.removeListener(this);
214+
if (isDesktopPlatform && canConfigureWindow) {
215+
windowManager.removeListener(this);
216+
}
213217
super.dispose();
214218
}
215219

lib/models/device.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@
1919

2020
import 'dart:convert';
2121

22+
import 'package:bluecherry_client/api/api.dart';
2223
import 'package:bluecherry_client/models/server.dart';
2324
import 'package:bluecherry_client/providers/server_provider.dart';
2425
import 'package:bluecherry_client/providers/settings_provider.dart';
2526
import 'package:bluecherry_client/utils/config.dart';
2627
import 'package:bluecherry_client/utils/extensions.dart';
2728
import 'package:bluecherry_client/widgets/device_grid/desktop/external_stream.dart';
2829
import 'package:flutter/foundation.dart';
29-
import 'package:http/http.dart' as http;
3030

3131
class ExternalDeviceData {
3232
final String? rackName;
@@ -265,7 +265,7 @@ class Device {
265265
queryParameters: data,
266266
);
267267

268-
var response = await http.get(uri);
268+
var response = await API.client.get(uri);
269269

270270
if (response.statusCode == 200) {
271271
var ret = json.decode(response.body) as Map;

lib/models/server.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import 'package:bluecherry_client/models/device.dart';
2121
import 'package:bluecherry_client/providers/settings_provider.dart';
2222
import 'package:bluecherry_client/utils/constants.dart';
2323
import 'package:bluecherry_client/utils/extensions.dart';
24+
import 'package:flutter/foundation.dart';
2425
import 'package:unity_video_player/unity_video_player.dart';
2526

2627
class AdditionalServerOptions {
@@ -214,6 +215,13 @@ class Server {
214215
return '$name;$ip;$port';
215216
}
216217

218+
/// Whether this server has been connected to before.
219+
bool get hasCookies {
220+
if (kIsWeb) return true;
221+
222+
return cookie != null && cookie!.isNotEmpty;
223+
}
224+
217225
@override
218226
String toString() =>
219227
'Server($name, $ip, $port, $rtspPort, $login, $password, $devices, $serverUUID, $cookie, $online, $passedCertificates)';

lib/providers/app_provider_interface.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
* along with this program. If not, see <http://www.gnu.org/licenses/>.
1818
*/
1919

20+
import 'package:bluecherry_client/utils/storage.dart';
2021
import 'package:flutter/widgets.dart';
2122
import 'package:safe_local_storage/safe_local_storage.dart';
2223

@@ -27,7 +28,7 @@ abstract class UnityProvider extends ChangeNotifier {
2728
@protected
2829
Future<void> initializeStorage(SafeLocalStorage storage, String key) async {
2930
try {
30-
final hive = await storage.read() as Map;
31+
final hive = await tryReadStorage(() => storage.read());
3132
if (!hive.containsKey(key)) {
3233
await save();
3334
} else {

0 commit comments

Comments
 (0)