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

Initial web support #204

Merged
merged 21 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
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 @@ -162,8 +162,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
Loading