Skip to content

Commit

Permalink
Initial web support (#204)
Browse files Browse the repository at this point in the history
  • Loading branch information
bdlukaa authored Feb 14, 2024
2 parents 9c46f68 + 12023d7 commit dce6167
Show file tree
Hide file tree
Showing 46 changed files with 1,141 additions and 232 deletions.
25 changes: 25 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -349,3 +349,28 @@ jobs:
run: |
flutterpi_tool build --release --cpu=pi4
build_web:
name: Bluecherry Web
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
submodules: recursive

- name: Install Flutter
uses: subosito/[email protected]
with:
channel: "stable"
cache: false

- name: Initiate Flutter
run: |
flutter gen-l10n
flutter pub get
- name: Build
run: |
flutter build web --verbose --dart-define=FLUTTER_WEB_USE_SKIA=true --dart-define=FLUTTER_WEB_AUTO_DETECT=true
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
.fvm/

# Web related
lib/generated_plugin_registrant.dart

# Symbolication related
app.*.symbols
Expand Down Expand Up @@ -66,4 +65,4 @@ AppDirassets/
*.tar.gz
rpmbuild/

bluecherry_config
bluecherry_config
14 changes: 7 additions & 7 deletions .metadata
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,20 @@
# This file should be version controlled and should not be manually edited.

version:
revision: "9e1c857886f07d342cf106f2cd588bcd5e031bb2"
channel: "stable"
revision: "984dc1947b574a51d5493e9c3b866a8218c69192"
channel: "master"

project_type: app

# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 9e1c857886f07d342cf106f2cd588bcd5e031bb2
base_revision: 9e1c857886f07d342cf106f2cd588bcd5e031bb2
- platform: macos
create_revision: 9e1c857886f07d342cf106f2cd588bcd5e031bb2
base_revision: 9e1c857886f07d342cf106f2cd588bcd5e031bb2
create_revision: 984dc1947b574a51d5493e9c3b866a8218c69192
base_revision: 984dc1947b574a51d5493e9c3b866a8218c69192
- platform: web
create_revision: 984dc1947b574a51d5493e9c3b866a8218c69192
base_revision: 984dc1947b574a51d5493e9c3b866a8218c69192

# User provided section

Expand Down
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,18 @@ flutter build [linux|windows|macos|android|ios]

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.

#### Linux

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).

```bash
flutter run --dart-define-from-file=linux/env/[DISTRO_ENV].json
flutter run -d linux --dart-define-from-file=linux/env/[DISTRO_ENV].json
```

#### Web

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:

```bash
flutter run -d chrome --web-browser-flag "--disable-web-security"
```
41 changes: 29 additions & 12 deletions lib/api/api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import 'package:bluecherry_client/api/api_helpers.dart';
import 'package:bluecherry_client/models/device.dart';
import 'package:bluecherry_client/models/server.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart';
import 'package:http/http.dart' as http;
import 'package:xml2json/xml2json.dart';

export 'events.dart';
Expand All @@ -32,6 +32,19 @@ export 'ptz.dart';
class API {
static final API instance = API();

static final client = http.Client();

static void initialize() {
if (kIsWeb) {
// On Web, a [BrowserClient] is used under the hood, which has the
// "withCredentials" property. This is cast as dynamic because the
// [BrowserClient] is not available on the other platforms.
//
// This is used to enable the cookies on the requests.
(client as dynamic).withCredentials = true;
}
}

/// Checks details of a [server] entered by the user.
/// If the attributes present in [Server] are correct, then the
/// returned object will have [Server.serverUUID] & [Server.cookie]
Expand All @@ -45,10 +58,10 @@ class API {
{
'login': server.login,
'password': server.password,
'from_client': 'true',
'from_client': '${true}',
},
);
final request = MultipartRequest('POST', uri)
final request = http.MultipartRequest('POST', uri)
..fields.addAll({
'login': server.login,
'password': server.password,
Expand All @@ -59,14 +72,18 @@ class API {
});
final response = await request.send();
final body = await response.stream.bytesToString();
debugPrint('checkServerCredentials ${response.statusCode}');
// debugPrint(response.headers.toString());
debugPrint(
'checkServerCredentials ${response.statusCode}'
'\n:....${response.headers}'
'\n:....$body',
);

if (response.statusCode == 200) {
final json = await compute(jsonDecode, body);
return server.copyWith(
serverUUID: json['server_uuid'],
cookie: response.headers['set-cookie'],
cookie:
response.headers['set-cookie'] ?? response.headers['Set-Cookie'],
online: true,
);
} else {
Expand All @@ -93,8 +110,8 @@ class API {
}

try {
assert(server.serverUUID != null && server.cookie != null);
final response = await get(
assert(server.serverUUID != null && server.hasCookies);
final response = await client.get(
Uri.https(
'${Uri.encodeComponent(server.login)}:${Uri.encodeComponent(server.password)}@${server.ip}:${server.port}',
'/devices.php',
Expand Down Expand Up @@ -144,8 +161,8 @@ class API {
///
Future<String?> getNotificationAPIEndpoint(Server server) async {
try {
assert(server.serverUUID != null && server.cookie != null);
final response = await get(
assert(server.serverUUID != null && server.hasCookies);
final response = await client.get(
Uri.https(
'${Uri.encodeComponent(server.login)}:${Uri.encodeComponent(server.password)}@${server.ip}:${server.port}',
'/mobile-app-config.json',
Expand Down Expand Up @@ -174,7 +191,7 @@ class API {
assert(uri != null, '[getNotificationAPIEndpoint] returned null.');
assert(clientID != null, '[clientUUID] returned null.');
assert(server.serverUUID != null, '[server.serverUUID] is null.');
final response = await post(
final response = await client.post(
Uri.parse('${uri!}store-token'),
headers: {
'Cookie': server.cookie!,
Expand Down Expand Up @@ -216,7 +233,7 @@ class API {
assert(uri != null, '[getNotificationAPIEndpoint] returned null.');
assert(clientID != null, '[clientUUID] returned null.');
assert(server.serverUUID != null, '[server.serverUUID] is null.');
final response = await post(
final response = await client.post(
Uri.parse('${uri!}remove-token'),
headers: {
'Cookie': server.cookie!,
Expand Down
3 changes: 1 addition & 2 deletions lib/api/api_helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import 'package:bluecherry_client/models/server.dart';
import 'package:bluecherry_client/providers/server_provider.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart';
import 'package:path_provider/path_provider.dart';

/// This file mainly contains helper functions for working with the API.
Expand Down Expand Up @@ -110,7 +109,7 @@ abstract class APIHelpers {
return 'file://$filePath';
// Download the event thumbnail only if it doesn't exist already.
} else {
final response = await get(uri);
final response = await API.client.get(uri);
debugPrint(response.statusCode.toString());
if (response.statusCode ~/ 100 == 2 /* OK */) {
await file.create(recursive: true);
Expand Down
5 changes: 2 additions & 3 deletions lib/api/events.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import 'package:bluecherry_client/models/event.dart';
import 'package:bluecherry_client/models/server.dart';
import 'package:bluecherry_client/utils/methods.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:xml2json/xml2json.dart';

extension EventsExtension on API {
Expand Down Expand Up @@ -70,8 +69,8 @@ extension EventsExtension on API {
'${deviceId != null ? 'for device $deviceId' : ''}',
);

assert(server.serverUUID != null && server.cookie != null);
final response = await http.get(
assert(server.serverUUID != null && server.hasCookies);
final response = await API.client.get(
Uri.https(
'${Uri.encodeComponent(server.login)}:${Uri.encodeComponent(server.password)}@${server.ip}:${server.port}',
'/events/',
Expand Down
5 changes: 2 additions & 3 deletions lib/api/ptz.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import 'package:bluecherry_client/api/api.dart';
import 'package:bluecherry_client/models/device.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:http/http.dart' as http;

enum PTZCommand {
move,
Expand Down Expand Up @@ -113,7 +112,7 @@ extension PtzApiExtension on API {

debugPrint(url.toString());

final response = await http.get(
final response = await API.client.get(
url,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Expand Down Expand Up @@ -156,7 +155,7 @@ extension PtzApiExtension on API {

debugPrint(url.toString());

final response = await http.get(
final response = await API.client.get(
url,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Expand Down
2 changes: 1 addition & 1 deletion lib/firebase_messaging_background_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ abstract class FirebaseConfiguration {
FirebaseMessaging.instance.getToken().then((token) async {
debugPrint('[FirebaseMessaging.instance.getToken]: $token');
if (token != null) {
final data = await storage.read() as Map;
final data = await tryReadStorage(() => storage.read());
// Do not proceed, if token is already saved.
if (data[kHiveNotificationToken] == token) {
return;
Expand Down
16 changes: 10 additions & 6 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:bluecherry_client/api/api.dart';
import 'package:bluecherry_client/api/api_helpers.dart';
import 'package:bluecherry_client/firebase_messaging_background_handler.dart';
import 'package:bluecherry_client/models/device.dart';
Expand All @@ -35,7 +36,7 @@ import 'package:bluecherry_client/providers/mobile_view_provider.dart';
import 'package:bluecherry_client/providers/server_provider.dart';
import 'package:bluecherry_client/providers/settings_provider.dart';
import 'package:bluecherry_client/providers/update_provider.dart';
import 'package:bluecherry_client/utils/app_links.dart' as app_links;
import 'package:bluecherry_client/utils/app_links/app_links.dart' as app_links;
import 'package:bluecherry_client/utils/logging.dart' as logging;
import 'package:bluecherry_client/utils/methods.dart';
import 'package:bluecherry_client/utils/storage.dart';
Expand Down Expand Up @@ -81,6 +82,7 @@ Future<void> main(List<String> args) async {
}

DevHttpOverrides.configureCertificates();
API.initialize();
await UnityVideoPlayerInterface.instance.initialize();
if (isDesktopPlatform && Platform.isLinux) {
if (UpdateManager.linuxEnvironment == LinuxPlatform.embedded) {
Expand Down Expand Up @@ -146,7 +148,7 @@ Future<void> main(List<String> args) async {
// Request notifications permission for iOS, Android 13+ and Windows.
//
// permission_handler only supports these platforms
if (isMobilePlatform || Platform.isWindows) {
if (kIsWeb || isMobilePlatform || Platform.isWindows) {
() async {
if (await Permission.notification.isDenied) {
final state = await Permission.notification.request();
Expand All @@ -170,8 +172,8 @@ Future<void> main(List<String> args) async {
UpdateManager.ensureInitialized(),
]);

/// Firebase messaging isn't available on Windows nor Linux
if (kIsWeb || isMobilePlatform || Platform.isMacOS) {
/// Firebase messaging isn't available on windows nor linux
if (!kIsWeb && (isMobilePlatform || Platform.isMacOS)) {
FirebaseConfiguration.ensureInitialized();
}

Expand All @@ -198,8 +200,8 @@ class _UnityAppState extends State<UnityApp>
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
windowManager.addListener(this);
if (isDesktopPlatform && canConfigureWindow) {
windowManager.addListener(this);
windowManager.setPreventClose(true).then((_) {
if (mounted) setState(() {});
});
Expand All @@ -209,7 +211,9 @@ class _UnityAppState extends State<UnityApp>
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
windowManager.removeListener(this);
if (isDesktopPlatform && canConfigureWindow) {
windowManager.removeListener(this);
}
super.dispose();
}

Expand Down
4 changes: 2 additions & 2 deletions lib/models/device.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@

import 'dart:convert';

import 'package:bluecherry_client/api/api.dart';
import 'package:bluecherry_client/models/server.dart';
import 'package:bluecherry_client/providers/server_provider.dart';
import 'package:bluecherry_client/providers/settings_provider.dart';
import 'package:bluecherry_client/utils/config.dart';
import 'package:bluecherry_client/utils/extensions.dart';
import 'package:bluecherry_client/widgets/device_grid/desktop/external_stream.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;

class ExternalDeviceData {
final String? rackName;
Expand Down Expand Up @@ -265,7 +265,7 @@ class Device {
queryParameters: data,
);

var response = await http.get(uri);
var response = await API.client.get(uri);

if (response.statusCode == 200) {
var ret = json.decode(response.body) as Map;
Expand Down
8 changes: 8 additions & 0 deletions lib/models/server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import 'package:bluecherry_client/models/device.dart';
import 'package:bluecherry_client/providers/settings_provider.dart';
import 'package:bluecherry_client/utils/constants.dart';
import 'package:bluecherry_client/utils/extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:unity_video_player/unity_video_player.dart';

class AdditionalServerOptions {
Expand Down Expand Up @@ -214,6 +215,13 @@ class Server {
return '$name;$ip;$port';
}

/// Whether this server has been connected to before.
bool get hasCookies {
if (kIsWeb) return true;

return cookie != null && cookie!.isNotEmpty;
}

@override
String toString() =>
'Server($name, $ip, $port, $rtspPort, $login, $password, $devices, $serverUUID, $cookie, $online, $passedCertificates)';
Expand Down
3 changes: 2 additions & 1 deletion lib/providers/app_provider_interface.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import 'package:bluecherry_client/utils/storage.dart';
import 'package:flutter/widgets.dart';
import 'package:safe_local_storage/safe_local_storage.dart';

Expand All @@ -27,7 +28,7 @@ abstract class UnityProvider extends ChangeNotifier {
@protected
Future<void> initializeStorage(SafeLocalStorage storage, String key) async {
try {
final hive = await storage.read() as Map;
final hive = await tryReadStorage(() => storage.read());
if (!hive.containsKey(key)) {
await save();
} else {
Expand Down
Loading

0 comments on commit dce6167

Please sign in to comment.