mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-11 15:01:02 +03:00
WebClient: 3.44 webcodecs offline (#15722)
* feat(web): zero-readback WebCodecs video path Decoded VideoFrames from js/src/webcodecs.js are handed to Flutter via window.onVideoFrame and imported GPU-side with createImageFromTextureSource; any failure unregisters the hook so the JS side falls back to RGBA readback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): load bundled terminal font when Google CDNs are unreachable In air-gapped deployments GoogleFonts.robotoMono() cannot download the terminal font; when index.html signals offline mode, load the copy bundled with the web app under the family name google_fonts registers. Part of the fix for rustdesk/rustdesk-server-pro#996. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: bump windows arm64 to Flutter 3.44.8, add web build patch script apply_flutter_3.44_web_patches.sh prepares a 3.44.x web build on top of the shared source patches: qr_code_scanner's web impl needs dart:ui_web for the removed platformViewRegistry, and flutter/web/fonts is refreshed to the font paths the 3.44 engine requests. The disabled build-rustdesk-web job runs it automatically once FLUTTER_VERSION moves to 3.44.x, and version-guarded 'Patch flutter' steps no longer fail when the guard does not match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): prevent stale WebCodecs frames across sessions Signed-off-by: fufesou <linlong1266@gmail.com> * fix(web): harden WebCodecs reconnect and Flutter 3.44 patches Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ci): harden Flutter 3.44 patch input validation Validate required files before checking patch state, parameterize the theme-range validator, and prevent missing inputs from satisfying NO_MATCHES checks. Signed-off-by: fufesou <linlong1266@gmail.com> * Remove unused code Signed-off-by: fufesou <linlong1266@gmail.com> * fix(web): retry font loading and dispose stale decoded images Signed-off-by: fufesou <linlong1266@gmail.com> * remove unused code Signed-off-by: fufesou <linlong1266@gmail.com> * fix(web): Bad state: RenderBox was not laid out Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
115
.github/patches/apply_flutter_3.44_source_patches.sh
vendored
115
.github/patches/apply_flutter_3.44_source_patches.sh
vendored
@@ -17,6 +17,109 @@
|
|||||||
# therefore CRLF-safe.
|
# therefore CRLF-safe.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
readonly NO_MATCHES=0
|
||||||
|
readonly SINGLE_MATCH=1
|
||||||
|
readonly THEME_MATCHES=2
|
||||||
|
|
||||||
|
has_exact_count() {
|
||||||
|
local -r expected_count="$1"
|
||||||
|
local -r pattern="$2"
|
||||||
|
local -r file="$3"
|
||||||
|
local actual_count
|
||||||
|
[[ -r "$file" ]] || return 1
|
||||||
|
actual_count="$(grep -cF "$pattern" "$file" || true)"
|
||||||
|
[[ "$actual_count" -eq "$expected_count" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# The target background-color line must directly follow DialogThemeData in the selected range.
|
||||||
|
has_dialog_background_in_theme_range() {
|
||||||
|
local -r start_pattern="$1"
|
||||||
|
local -r end_pattern="$2"
|
||||||
|
local -r target_pattern="$3"
|
||||||
|
local -r file="$4"
|
||||||
|
awk -v start_pattern="$start_pattern" \
|
||||||
|
-v end_pattern="$end_pattern" \
|
||||||
|
-v target_pattern="$target_pattern" '
|
||||||
|
index($0, start_pattern) {
|
||||||
|
in_theme = 1
|
||||||
|
next
|
||||||
|
}
|
||||||
|
in_theme && index($0, end_pattern) {
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
in_theme && index($0, "dialogTheme: DialogThemeData(") {
|
||||||
|
if (getline > 0) {
|
||||||
|
line = $0
|
||||||
|
sub(/\r$/, "", line)
|
||||||
|
sub(/^[[:space:]]+/, "", line)
|
||||||
|
matched = line == target_pattern
|
||||||
|
}
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
END {
|
||||||
|
exit matched ? 0 : 1
|
||||||
|
}
|
||||||
|
' "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_patch_inputs() {
|
||||||
|
if [[ ! -f flutter/lib/common.dart || ! -r flutter/lib/common.dart ]]; then
|
||||||
|
echo "Flutter 3.44 source patch input is missing or unreadable: flutter/lib/common.dart" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ ! -f flutter/pubspec.yaml || ! -r flutter/pubspec.yaml ]]; then
|
||||||
|
echo "Flutter 3.44 source patch input is missing or unreadable: flutter/pubspec.yaml" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
is_complete_patch_state() {
|
||||||
|
has_exact_count "$THEME_MATCHES" 'dialogTheme: DialogThemeData(' flutter/lib/common.dart &&
|
||||||
|
has_exact_count "$THEME_MATCHES" 'tabBarTheme: const TabBarThemeData(' flutter/lib/common.dart &&
|
||||||
|
has_exact_count "$SINGLE_MATCH" 'backgroundColor: Colors.white,' flutter/lib/common.dart &&
|
||||||
|
has_exact_count "$SINGLE_MATCH" 'backgroundColor: Color(0xFF18191E),' flutter/lib/common.dart &&
|
||||||
|
has_exact_count "$SINGLE_MATCH" 'extended_text: 15.0.2' flutter/pubspec.yaml &&
|
||||||
|
has_exact_count "$SINGLE_MATCH" 'google_fonts: ^8.1.0' flutter/pubspec.yaml &&
|
||||||
|
has_exact_count "$NO_MATCHES" 'dialogTheme: DialogTheme(' flutter/lib/common.dart &&
|
||||||
|
has_exact_count "$NO_MATCHES" 'tabBarTheme: const TabBarTheme(' flutter/lib/common.dart &&
|
||||||
|
has_exact_count "$NO_MATCHES" 'extended_text: 14.0.0' flutter/pubspec.yaml &&
|
||||||
|
has_exact_count "$NO_MATCHES" 'google_fonts: ^6.2.1' flutter/pubspec.yaml &&
|
||||||
|
has_dialog_background_in_theme_range 'static ThemeData lightTheme = ThemeData(' \
|
||||||
|
'static ThemeData darkTheme = ThemeData(' 'backgroundColor: Colors.white,' \
|
||||||
|
flutter/lib/common.dart &&
|
||||||
|
has_dialog_background_in_theme_range 'static ThemeData darkTheme = ThemeData(' \
|
||||||
|
'scrollbarTheme: scrollbarThemeDark,' 'backgroundColor: Color(0xFF18191E),' \
|
||||||
|
flutter/lib/common.dart
|
||||||
|
}
|
||||||
|
|
||||||
|
is_unpatched_state() {
|
||||||
|
has_exact_count "$THEME_MATCHES" 'dialogTheme: DialogTheme(' flutter/lib/common.dart &&
|
||||||
|
has_exact_count "$THEME_MATCHES" 'tabBarTheme: const TabBarTheme(' flutter/lib/common.dart &&
|
||||||
|
has_exact_count "$SINGLE_MATCH" 'extended_text: 14.0.0' flutter/pubspec.yaml &&
|
||||||
|
has_exact_count "$SINGLE_MATCH" 'google_fonts: ^6.2.1' flutter/pubspec.yaml &&
|
||||||
|
has_exact_count "$NO_MATCHES" 'dialogTheme: DialogThemeData(' flutter/lib/common.dart &&
|
||||||
|
has_exact_count "$NO_MATCHES" 'tabBarTheme: const TabBarThemeData(' flutter/lib/common.dart &&
|
||||||
|
has_exact_count "$NO_MATCHES" 'backgroundColor: Colors.white,' flutter/lib/common.dart &&
|
||||||
|
has_exact_count "$NO_MATCHES" 'backgroundColor: Color(0xFF18191E),' flutter/lib/common.dart &&
|
||||||
|
has_exact_count "$NO_MATCHES" 'extended_text: 15.0.2' flutter/pubspec.yaml &&
|
||||||
|
has_exact_count "$NO_MATCHES" 'google_fonts: ^8.1.0' flutter/pubspec.yaml
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! validate_patch_inputs; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if is_complete_patch_state; then
|
||||||
|
echo "Flutter 3.44 source patches already applied."
|
||||||
|
git --no-pager diff -- flutter/lib/common.dart flutter/pubspec.yaml
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! is_unpatched_state; then
|
||||||
|
echo "Flutter 3.44 source patches are partially applied or their anchors have drifted." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# ThemeData API renames (Flutter 3.27+):
|
# ThemeData API renames (Flutter 3.27+):
|
||||||
sed -i 's/dialogTheme: DialogTheme(/dialogTheme: DialogThemeData(/g' flutter/lib/common.dart
|
sed -i 's/dialogTheme: DialogTheme(/dialogTheme: DialogThemeData(/g' flutter/lib/common.dart
|
||||||
sed -i 's/tabBarTheme: const TabBarTheme(/tabBarTheme: const TabBarThemeData(/g' flutter/lib/common.dart
|
sed -i 's/tabBarTheme: const TabBarTheme(/tabBarTheme: const TabBarThemeData(/g' flutter/lib/common.dart
|
||||||
@@ -28,12 +131,10 @@ sed -i '/static ThemeData darkTheme = ThemeData(/,/scrollbarTheme: scrollbarThem
|
|||||||
sed -i 's/extended_text: 14.0.0/extended_text: 15.0.2/' flutter/pubspec.yaml
|
sed -i 's/extended_text: 14.0.0/extended_text: 15.0.2/' flutter/pubspec.yaml
|
||||||
sed -i 's/google_fonts: \^6.2.1/google_fonts: ^8.1.0/' flutter/pubspec.yaml
|
sed -i 's/google_fonts: \^6.2.1/google_fonts: ^8.1.0/' flutter/pubspec.yaml
|
||||||
|
|
||||||
# Fail loudly if any expected string drifted, so we never silently build unpatched:
|
# Fail loudly if any expected substitution did not produce the complete state.
|
||||||
grep -qF 'dialogTheme: DialogThemeData(' flutter/lib/common.dart
|
if ! is_complete_patch_state; then
|
||||||
grep -qF 'tabBarTheme: const TabBarThemeData(' flutter/lib/common.dart
|
echo "Flutter 3.44 source patches did not produce the expected state." >&2
|
||||||
grep -qF 'backgroundColor: Colors.white,' flutter/lib/common.dart
|
exit 1
|
||||||
grep -qF 'backgroundColor: Color(0xFF18191E),' flutter/lib/common.dart
|
fi
|
||||||
grep -qF 'extended_text: 15.0.2' flutter/pubspec.yaml
|
|
||||||
grep -qF 'google_fonts: ^8.1.0' flutter/pubspec.yaml
|
|
||||||
|
|
||||||
git --no-pager diff -- flutter/lib/common.dart flutter/pubspec.yaml
|
git --no-pager diff -- flutter/lib/common.dart flutter/pubspec.yaml
|
||||||
|
|||||||
51
.github/patches/apply_flutter_3.44_web_patches.sh
vendored
Executable file
51
.github/patches/apply_flutter_3.44_web_patches.sh
vendored
Executable file
@@ -0,0 +1,51 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Prepares a web build on Flutter 3.44.x. Companion to
|
||||||
|
# apply_flutter_3.44_source_patches.sh (which it runs first): the web target
|
||||||
|
# additionally needs qr_code_scanner's web implementation patched for the
|
||||||
|
# dart:ui platformViewRegistry removal, and flutter/web/fonts refreshed with
|
||||||
|
# the font paths the 3.44 engine requests for offline/air-gapped support
|
||||||
|
# (rustdesk-server-pro#996; see flutter/web/fonts/sync_fonts.py).
|
||||||
|
#
|
||||||
|
# Run from the repository root with Flutter 3.44.x on PATH, then build:
|
||||||
|
# bash .github/patches/apply_flutter_3.44_web_patches.sh
|
||||||
|
# (cd flutter && flutter build web --release) # or ./web/js/flutter_build.py
|
||||||
|
#
|
||||||
|
# Idempotent. To undo the source changes locally:
|
||||||
|
# git checkout -- flutter/lib/common.dart flutter/pubspec.yaml flutter/pubspec.lock
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
flutter --version | grep -q "Flutter 3\.44\." || {
|
||||||
|
echo "Flutter 3.44.x must be on PATH; found:" >&2
|
||||||
|
flutter --version | grep "^Flutter" >&2 || true
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Shared 3.44 source/pubspec patches own their complete-state validation.
|
||||||
|
bash .github/patches/apply_flutter_3.44_source_patches.sh
|
||||||
|
|
||||||
|
# Populate the pub cache with the 3.44 dependency resolution.
|
||||||
|
(cd flutter && flutter pub get)
|
||||||
|
|
||||||
|
# qr_code_scanner 1.0.1 (unmaintained) reads platformViewRegistry from
|
||||||
|
# dart:ui, which Flutter 3.44 removed; point it at dart:ui_web instead. The
|
||||||
|
# patched file also compiles on Flutter 3.24 (dart:ui_web exists there), so
|
||||||
|
# mutating the shared pub cache is safe for other local builds.
|
||||||
|
QR_WEB="${PUB_CACHE:-$HOME/.pub-cache}/hosted/pub.dev/qr_code_scanner-1.0.1/lib/src/web/flutter_qr_web.dart"
|
||||||
|
if ! grep -qF "dart:ui_web" "$QR_WEB"; then
|
||||||
|
sed -i.bak "s|import 'dart:ui' as ui;|import 'dart:ui' as ui; import 'dart:ui_web' as ui_web;|" "$QR_WEB"
|
||||||
|
rm -f "$QR_WEB.bak"
|
||||||
|
fi
|
||||||
|
if grep -qF "ui.platformViewRegistry" "$QR_WEB"; then
|
||||||
|
sed -i.bak "s|ui\.platformViewRegistry|ui_web.platformViewRegistry|g" "$QR_WEB"
|
||||||
|
rm -f "$QR_WEB.bak"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Mirror the fonts this engine version requests into flutter/web/fonts.
|
||||||
|
python3 flutter/web/fonts/sync_fonts.py
|
||||||
|
|
||||||
|
# Fail loudly if any expected state is missing:
|
||||||
|
grep -qF "import 'dart:ui' as ui; import 'dart:ui_web' as ui_web;" "$QR_WEB"
|
||||||
|
grep -qF "ui_web.platformViewRegistry" "$QR_WEB"
|
||||||
|
grep -qF 'google_fonts: ^8.1.0' flutter/pubspec.yaml
|
||||||
|
|
||||||
|
echo "Flutter 3.44 web patches applied."
|
||||||
2
.github/workflows/bridge.yml
vendored
2
.github/workflows/bridge.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
|||||||
target: x86_64-unknown-linux-gnu,
|
target: x86_64-unknown-linux-gnu,
|
||||||
os: ubuntu-22.04,
|
os: ubuntu-22.04,
|
||||||
extra-build-args: "",
|
extra-build-args: "",
|
||||||
flutter-version: "3.44.0",
|
flutter-version: "3.44.8",
|
||||||
artifact-name: "bridge-artifact-flutter-3.44",
|
artifact-name: "bridge-artifact-flutter-3.44",
|
||||||
}
|
}
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
35
.github/workflows/flutter-build.yml
vendored
35
.github/workflows/flutter-build.yml
vendored
@@ -31,7 +31,7 @@ env:
|
|||||||
# engine is 3.44. Every other platform stays on FLUTTER_VERSION (3.24.5) until Windows 7
|
# engine is 3.44. Every other platform stays on FLUTTER_VERSION (3.24.5) until Windows 7
|
||||||
# support is restored after the upstream-wide Flutter bump. The arm64 job patches the few
|
# support is restored after the upstream-wide Flutter bump. The arm64 job patches the few
|
||||||
# 3.44-only source/pubspec changes on the fly (see "Patch RustDesk sources for Flutter 3.44").
|
# 3.44-only source/pubspec changes on the fly (see "Patch RustDesk sources for Flutter 3.44").
|
||||||
FLUTTER_WINDOWS_ARM_VERSION: "3.44.0"
|
FLUTTER_WINDOWS_ARM_VERSION: "3.44.8"
|
||||||
# for arm64 linux because official Dart SDK does not work
|
# for arm64 linux because official Dart SDK does not work
|
||||||
FLUTTER_ELINUX_VERSION: "3.16.9"
|
FLUTTER_ELINUX_VERSION: "3.16.9"
|
||||||
TAG_NAME: "${{ inputs.upload-tag }}"
|
TAG_NAME: "${{ inputs.upload-tag }}"
|
||||||
@@ -224,7 +224,9 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cp .github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff $(dirname $(dirname $(which flutter)))
|
cp .github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff $(dirname $(dirname $(which flutter)))
|
||||||
cd $(dirname $(dirname $(which flutter)))
|
cd $(dirname $(dirname $(which flutter)))
|
||||||
[[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply flutter_3.24.4_dropdown_menu_enableFilter.diff
|
if [[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]]; then
|
||||||
|
git apply flutter_3.24.4_dropdown_menu_enableFilter.diff
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Patch RustDesk sources for Flutter 3.44 (arm64)
|
- name: Patch RustDesk sources for Flutter 3.44 (arm64)
|
||||||
# arm64 is the only target on Flutter 3.44; apply its source/pubspec deltas on the fly
|
# arm64 is the only target on Flutter 3.44; apply its source/pubspec deltas on the fly
|
||||||
@@ -595,7 +597,9 @@ jobs:
|
|||||||
- name: Patch flutter
|
- name: Patch flutter
|
||||||
run: |
|
run: |
|
||||||
cd $(dirname $(dirname $(which flutter)))
|
cd $(dirname $(dirname $(which flutter)))
|
||||||
[[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
|
if [[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]]; then
|
||||||
|
git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Setup vcpkg with Github Actions binary cache
|
- name: Setup vcpkg with Github Actions binary cache
|
||||||
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
|
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
|
||||||
@@ -774,7 +778,9 @@ jobs:
|
|||||||
- name: Patch flutter
|
- name: Patch flutter
|
||||||
run: |
|
run: |
|
||||||
cd $(dirname $(dirname $(which flutter)))
|
cd $(dirname $(dirname $(which flutter)))
|
||||||
[[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
|
if [[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]]; then
|
||||||
|
git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Workaround for flutter issue
|
- name: Workaround for flutter issue
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -1033,7 +1039,9 @@ jobs:
|
|||||||
- name: Patch flutter
|
- name: Patch flutter
|
||||||
run: |
|
run: |
|
||||||
cd $(dirname $(dirname $(which flutter)))
|
cd $(dirname $(dirname $(which flutter)))
|
||||||
[[ "3.24.5" == ${{env.ANDROID_FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
|
if [[ "3.24.5" == ${{env.ANDROID_FLUTTER_VERSION}} ]]; then
|
||||||
|
git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
|
||||||
|
fi
|
||||||
|
|
||||||
- uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1
|
- uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1
|
||||||
id: setup-ndk
|
id: setup-ndk
|
||||||
@@ -1305,7 +1313,9 @@ jobs:
|
|||||||
- name: Patch flutter
|
- name: Patch flutter
|
||||||
run: |
|
run: |
|
||||||
cd $(dirname $(dirname $(which flutter)))
|
cd $(dirname $(dirname $(which flutter)))
|
||||||
[[ "3.24.5" == ${{env.ANDROID_FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
|
if [[ "3.24.5" == ${{env.ANDROID_FLUTTER_VERSION}} ]]; then
|
||||||
|
git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Restore bridge files
|
- name: Restore bridge files
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
@@ -2413,7 +2423,18 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
cd $(dirname $(dirname $(which flutter)))
|
cd $(dirname $(dirname $(which flutter)))
|
||||||
[[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
|
if [[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]]; then
|
||||||
|
git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Patch sources for Flutter 3.44 web
|
||||||
|
# No-op while the web stays on Flutter 3.24.5; makes this job work as-is
|
||||||
|
# once FLUTTER_VERSION moves to 3.44.x (qr_code_scanner + fonts, see script).
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [[ "${{ env.FLUTTER_VERSION }}" == 3.44.* ]]; then
|
||||||
|
bash .github/patches/apply_flutter_3.44_web_patches.sh
|
||||||
|
fi
|
||||||
|
|
||||||
# https://rustdesk.com/docs/en/dev/build/web/
|
# https://rustdesk.com/docs/en/dev/build/web/
|
||||||
- name: Build web
|
- name: Build web
|
||||||
|
|||||||
@@ -588,7 +588,8 @@ _registerEventHandler() {
|
|||||||
|
|
||||||
Widget keyListenerBuilder(BuildContext context, Widget? child) {
|
Widget keyListenerBuilder(BuildContext context, Widget? child) {
|
||||||
return RawKeyboardListener(
|
return RawKeyboardListener(
|
||||||
focusNode: FocusNode(),
|
// `skipTraversal: isWeb` is to fix "Bad state: RenderBox was not laid out: minified:aeL#c19e4"
|
||||||
|
focusNode: FocusNode(skipTraversal: isWeb),
|
||||||
child: child ?? Container(),
|
child: child ?? Container(),
|
||||||
onKey: (RawKeyEvent event) {
|
onKey: (RawKeyEvent event) {
|
||||||
if (event.logicalKey == LogicalKeyboardKey.shiftLeft) {
|
if (event.logicalKey == LogicalKeyboardKey.shiftLeft) {
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import 'package:flutter_hbb/models/model.dart';
|
|||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
import 'package:flutter_hbb/models/terminal_model.dart';
|
import 'package:flutter_hbb/models/terminal_model.dart';
|
||||||
import 'package:flutter_hbb/mobile/terminal_keyboard_utils.dart';
|
import 'package:flutter_hbb/mobile/terminal_keyboard_utils.dart';
|
||||||
|
import 'package:flutter_hbb/web/dummy.dart'
|
||||||
|
if (dart.library.html) 'package:flutter_hbb/web/terminal_font.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:xterm/xterm.dart';
|
import 'package:xterm/xterm.dart';
|
||||||
import '../../desktop/pages/terminal_connection_manager.dart';
|
import '../../desktop/pages/terminal_connection_manager.dart';
|
||||||
@@ -67,6 +69,10 @@ class _TerminalPageState extends State<TerminalPage>
|
|||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
|
||||||
|
if (isWeb) {
|
||||||
|
loadLocalTerminalFontIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'[TerminalPage] Initializing terminal ${widget.terminalId} for peer ${widget.id}');
|
'[TerminalPage] Initializing terminal ${widget.terminalId} for peer ${widget.id}');
|
||||||
|
|
||||||
|
|||||||
@@ -1952,6 +1952,12 @@ class ImageModel with ChangeNotifier {
|
|||||||
platformFFI.nextRgba(sessionId, display);
|
platformFFI.nextRgba(sessionId, display);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// web only: image already created from a decoded WebCodecs frame
|
||||||
|
Future<void> onImage(
|
||||||
|
int display, ui.Image image, bool Function() isCurrentSession) async {
|
||||||
|
await update(image, isCurrentSession: isCurrentSession);
|
||||||
|
}
|
||||||
|
|
||||||
decodeAndUpdate(int display, Uint8List rgba) async {
|
decodeAndUpdate(int display, Uint8List rgba) async {
|
||||||
final pid = parent.target?.id;
|
final pid = parent.target?.id;
|
||||||
final rect = parent.target?.ffiModel.pi.getDisplayRect(display);
|
final rect = parent.target?.ffiModel.pi.getDisplayRect(display);
|
||||||
@@ -1963,11 +1969,16 @@ class ImageModel with ChangeNotifier {
|
|||||||
? ui.PixelFormat.rgba8888
|
? ui.PixelFormat.rgba8888
|
||||||
: ui.PixelFormat.bgra8888,
|
: ui.PixelFormat.bgra8888,
|
||||||
);
|
);
|
||||||
if (parent.target?.id != pid) return;
|
if (parent.target?.id != pid) {
|
||||||
|
image?.dispose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
await update(image);
|
await update(image);
|
||||||
}
|
}
|
||||||
|
|
||||||
update(ui.Image? image) async {
|
Future<void> update(ui.Image? image,
|
||||||
|
{bool Function()? isCurrentSession}) async {
|
||||||
|
if (_disposeIfStale(image, isCurrentSession)) return;
|
||||||
if (_image == null && image != null) {
|
if (_image == null && image != null) {
|
||||||
if (isDesktop || isWebDesktop) {
|
if (isDesktop || isWebDesktop) {
|
||||||
await parent.target?.canvasModel.updateViewStyle();
|
await parent.target?.canvasModel.updateViewStyle();
|
||||||
@@ -1978,11 +1989,19 @@ class ImageModel with ChangeNotifier {
|
|||||||
await initializeCursorAndCanvas(parent.target!);
|
await initializeCursorAndCanvas(parent.target!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (_disposeIfStale(image, isCurrentSession)) return;
|
||||||
_image?.dispose();
|
_image?.dispose();
|
||||||
_image = image;
|
_image = image;
|
||||||
if (image != null) notifyListeners();
|
if (image != null) notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _disposeIfStale(ui.Image? image, bool Function()? isCurrentSession) {
|
||||||
|
if (image == null || isCurrentSession == null) return false;
|
||||||
|
if (isCurrentSession()) return false;
|
||||||
|
image.dispose();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// mobile only
|
// mobile only
|
||||||
double get maxScale {
|
double get maxScale {
|
||||||
if (_image == null) return 1.5;
|
if (_image == null) return 1.5;
|
||||||
@@ -3853,6 +3872,15 @@ class FFI {
|
|||||||
onEvent2UIRgba();
|
onEvent2UIRgba();
|
||||||
imageModel.onRgba(display, data);
|
imageModel.onRgba(display, data);
|
||||||
});
|
});
|
||||||
|
platformFFI.setVideoFrameCallback((int display, ui.Image image,
|
||||||
|
bool Function() isCurrentSession) async {
|
||||||
|
if (!isCurrentSession()) {
|
||||||
|
image.dispose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await onEvent2UIRgba();
|
||||||
|
await imageModel.onImage(display, image, isCurrentSession);
|
||||||
|
});
|
||||||
this.id = id;
|
this.id = id;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -3940,7 +3968,7 @@ class FFI {
|
|||||||
this.id = id;
|
this.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
void onEvent2UIRgba() async {
|
Future<void> onEvent2UIRgba() async {
|
||||||
if (ffiModel.waitForImageDialogShow.isTrue) {
|
if (ffiModel.waitForImageDialogShow.isTrue) {
|
||||||
ffiModel.waitForImageDialogShow.value = false;
|
ffiModel.waitForImageDialogShow.value = false;
|
||||||
ffiModel.waitForImageTimer?.cancel();
|
ffiModel.waitForImageTimer?.cancel();
|
||||||
@@ -3996,6 +4024,9 @@ class FFI {
|
|||||||
/// Close the remote session.
|
/// Close the remote session.
|
||||||
Future<void> close({bool closeSession = true}) async {
|
Future<void> close({bool closeSession = true}) async {
|
||||||
closed = true;
|
closed = true;
|
||||||
|
if (isWeb) {
|
||||||
|
platformFFI.clearVideoFrameCallback();
|
||||||
|
}
|
||||||
chatModel.close();
|
chatModel.close();
|
||||||
// Close all terminal models
|
// Close all terminal models
|
||||||
for (final model in _terminalModels.values) {
|
for (final model in _terminalModels.values) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:ffi';
|
import 'dart:ffi';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:external_path/external_path.dart';
|
import 'package:external_path/external_path.dart';
|
||||||
@@ -283,6 +284,12 @@ class PlatformFFI {
|
|||||||
|
|
||||||
void setRgbaCallback(void Function(int, Uint8List) fun) async {}
|
void setRgbaCallback(void Function(int, Uint8List) fun) async {}
|
||||||
|
|
||||||
|
// web only, decoded WebCodecs frames arriving as ready-made images
|
||||||
|
void setVideoFrameCallback(
|
||||||
|
Future<void> Function(int, ui.Image, bool Function()) fun) {}
|
||||||
|
|
||||||
|
void clearVideoFrameCallback() {}
|
||||||
|
|
||||||
void startDesktopWebListener() {}
|
void startDesktopWebListener() {}
|
||||||
|
|
||||||
void stopDesktopWebListener() {}
|
void stopDesktopWebListener() {}
|
||||||
|
|||||||
@@ -2,14 +2,18 @@
|
|||||||
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:js_interop';
|
import 'dart:js_interop';
|
||||||
|
import 'dart:js_interop_unsafe';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'dart:js';
|
import 'dart:js';
|
||||||
import 'dart:html';
|
import 'dart:html';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
import 'dart:ui_web' as ui_web;
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_hbb/common/widgets/login.dart';
|
import 'package:flutter_hbb/common/widgets/login.dart';
|
||||||
import 'package:flutter_hbb/models/state_model.dart';
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
|
import 'package:flutter_hbb/models/web_video_frame_queue.dart';
|
||||||
|
|
||||||
import 'package:flutter_hbb/web/bridge.dart';
|
import 'package:flutter_hbb/web/bridge.dart';
|
||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
@@ -18,6 +22,22 @@ import 'package:uuid/uuid.dart';
|
|||||||
final List<StreamSubscription<MouseEvent>> mouseListeners = [];
|
final List<StreamSubscription<MouseEvent>> mouseListeners = [];
|
||||||
final List<StreamSubscription<KeyboardEvent>> keyListeners = [];
|
final List<StreamSubscription<KeyboardEvent>> keyListeners = [];
|
||||||
|
|
||||||
|
// WebCodecs VideoFrames handed over from js/src/webcodecs.js arrive as plain
|
||||||
|
// interop objects (the package language version predates extension types).
|
||||||
|
// This side owns each frame and must close it quickly: hardware decoders
|
||||||
|
// stall once their small output frame pool is exhausted.
|
||||||
|
int _videoFrameWidth(JSObject frame) =>
|
||||||
|
frame.getProperty<JSNumber>('displayWidth'.toJS).toDartInt;
|
||||||
|
int _videoFrameHeight(JSObject frame) =>
|
||||||
|
frame.getProperty<JSNumber>('displayHeight'.toJS).toDartInt;
|
||||||
|
void _closeVideoFrame(JSObject frame) {
|
||||||
|
try {
|
||||||
|
frame.callMethod<JSAny?>('close'.toJS);
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint('VideoFrame.close failed: $error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
typedef HandleEvent = Future<void> Function(Map<String, dynamic> evt);
|
typedef HandleEvent = Future<void> Function(Map<String, dynamic> evt);
|
||||||
|
|
||||||
class PlatformFFI {
|
class PlatformFFI {
|
||||||
@@ -33,6 +53,13 @@ class PlatformFFI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PlatformFFI._() {
|
PlatformFFI._() {
|
||||||
|
_videoFrameQueue = WebVideoFrameQueue(
|
||||||
|
importFrame: _importVideoFrame,
|
||||||
|
closeFrame: _closeVideoFrame,
|
||||||
|
disposeImage: (image) => image.dispose(),
|
||||||
|
onImportError: _handleVideoFrameImportError,
|
||||||
|
onCallbackError: _handleVideoImageCallbackError,
|
||||||
|
);
|
||||||
window.document.addEventListener(
|
window.document.addEventListener(
|
||||||
'visibilitychange',
|
'visibilitychange',
|
||||||
(event) => {
|
(event) => {
|
||||||
@@ -162,6 +189,46 @@ class PlatformFFI {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
late final WebVideoFrameQueue<JSObject, ui.Image> _videoFrameQueue;
|
||||||
|
|
||||||
|
// Zero-readback video path: the JS decoder hands decoded VideoFrames here
|
||||||
|
// (checking typeof window.onVideoFrame before every frame), and the engine
|
||||||
|
// imports them GPU-to-GPU via createImageBitmap. Unregistering the JS global
|
||||||
|
// reverts the JS side to the RGBA readback path.
|
||||||
|
void setVideoFrameCallback(
|
||||||
|
Future<void> Function(int, ui.Image, bool Function()) fun) {
|
||||||
|
_videoFrameQueue.beginSession(fun);
|
||||||
|
if (!_videoFrameQueue.isEnabled) return;
|
||||||
|
globalContext.setProperty(
|
||||||
|
'onVideoFrame'.toJS,
|
||||||
|
((JSNumber display, JSObject frame) {
|
||||||
|
_videoFrameQueue.submit(display.toDartInt, frame);
|
||||||
|
}).toJS,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearVideoFrameCallback() {
|
||||||
|
_videoFrameQueue.endSession();
|
||||||
|
globalContext.setProperty('onVideoFrame'.toJS, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ui.Image> _importVideoFrame(JSObject frame) async {
|
||||||
|
return await ui_web.createImageFromTextureSource(frame,
|
||||||
|
width: _videoFrameWidth(frame), height: _videoFrameHeight(frame));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleVideoFrameImportError(Object error, StackTrace stackTrace) {
|
||||||
|
debugPrintStack(
|
||||||
|
label: 'createImageFromTextureSource failed, using RGBA path: $error',
|
||||||
|
stackTrace: stackTrace);
|
||||||
|
globalContext.setProperty('onVideoFrame'.toJS, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleVideoImageCallbackError(Object error, StackTrace stackTrace) {
|
||||||
|
debugPrintStack(
|
||||||
|
label: 'video image callback error: $error', stackTrace: stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
void startDesktopWebListener() {
|
void startDesktopWebListener() {
|
||||||
mouseListeners.add(
|
mouseListeners.add(
|
||||||
window.document.onContextMenu.listen((evt) => evt.preventDefault()));
|
window.document.onContextMenu.listen((evt) => evt.preventDefault()));
|
||||||
|
|||||||
133
flutter/lib/models/web_video_frame_queue.dart
Normal file
133
flutter/lib/models/web_video_frame_queue.dart
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
typedef VideoFrameImporter<Frame, Image> = Future<Image> Function(Frame frame);
|
||||||
|
typedef VideoFrameCloser<Frame> = void Function(Frame frame);
|
||||||
|
typedef VideoImageDisposer<Image> = void Function(Image image);
|
||||||
|
typedef VideoSessionValidator = bool Function();
|
||||||
|
typedef VideoImageCallback<Image> = Future<void> Function(
|
||||||
|
int display, Image image, VideoSessionValidator isCurrentSession);
|
||||||
|
typedef VideoQueueErrorCallback = void Function(
|
||||||
|
Object error, StackTrace stackTrace);
|
||||||
|
|
||||||
|
class WebVideoFrameQueue<Frame, Image> {
|
||||||
|
WebVideoFrameQueue({
|
||||||
|
required VideoFrameImporter<Frame, Image> importFrame,
|
||||||
|
required VideoFrameCloser<Frame> closeFrame,
|
||||||
|
required VideoImageDisposer<Image> disposeImage,
|
||||||
|
required VideoQueueErrorCallback onImportError,
|
||||||
|
required VideoQueueErrorCallback onCallbackError,
|
||||||
|
}) : _importFrame = importFrame,
|
||||||
|
_closeFrame = closeFrame,
|
||||||
|
_disposeImage = disposeImage,
|
||||||
|
_onImportError = onImportError,
|
||||||
|
_onCallbackError = onCallbackError;
|
||||||
|
|
||||||
|
final VideoFrameImporter<Frame, Image> _importFrame;
|
||||||
|
final VideoFrameCloser<Frame> _closeFrame;
|
||||||
|
final VideoImageDisposer<Image> _disposeImage;
|
||||||
|
final VideoQueueErrorCallback _onImportError;
|
||||||
|
final VideoQueueErrorCallback _onCallbackError;
|
||||||
|
final Map<int, _QueuedFrame<Frame>> _pending = {};
|
||||||
|
|
||||||
|
VideoImageCallback<Image>? _callback;
|
||||||
|
int _generation = 0;
|
||||||
|
bool _processing = false;
|
||||||
|
bool _enabled = true;
|
||||||
|
|
||||||
|
bool get isEnabled => _enabled;
|
||||||
|
|
||||||
|
void beginSession(VideoImageCallback<Image> callback) {
|
||||||
|
_invalidateSession();
|
||||||
|
_enabled = true;
|
||||||
|
_callback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
void endSession() {
|
||||||
|
_invalidateSession();
|
||||||
|
_callback = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _invalidateSession() {
|
||||||
|
_generation++;
|
||||||
|
for (final queued in _pending.values) {
|
||||||
|
_closeFrame(queued.frame);
|
||||||
|
}
|
||||||
|
_pending.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool submit(int display, Frame frame) {
|
||||||
|
if (!_enabled || _callback == null) {
|
||||||
|
_closeFrame(frame);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final replaced = _pending.remove(display);
|
||||||
|
if (replaced != null) {
|
||||||
|
_closeFrame(replaced.frame);
|
||||||
|
}
|
||||||
|
_pending[display] = _QueuedFrame(display, frame, _generation);
|
||||||
|
_startProcessing();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startProcessing() {
|
||||||
|
if (_processing) return;
|
||||||
|
_processing = true;
|
||||||
|
unawaited(Future<void>(_process));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _process() async {
|
||||||
|
while (_pending.isNotEmpty) {
|
||||||
|
final display = _pending.keys.first;
|
||||||
|
final queued = _pending.remove(display)!;
|
||||||
|
if (!_enabled || queued.generation != _generation) {
|
||||||
|
_closeFrame(queued.frame);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await _importAndDeliver(queued);
|
||||||
|
}
|
||||||
|
_processing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _importAndDeliver(_QueuedFrame<Frame> queued) async {
|
||||||
|
Image? image;
|
||||||
|
try {
|
||||||
|
image = await _importFrame(queued.frame);
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
if (queued.generation == _generation) {
|
||||||
|
_enabled = false;
|
||||||
|
_onImportError(error, stackTrace);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_closeFrame(queued.frame);
|
||||||
|
}
|
||||||
|
if (image != null) {
|
||||||
|
await _deliver(queued, image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _deliver(_QueuedFrame<Frame> queued, Image image) async {
|
||||||
|
final callback = _callback;
|
||||||
|
bool isCurrentSession() =>
|
||||||
|
_enabled &&
|
||||||
|
queued.generation == _generation &&
|
||||||
|
identical(callback, _callback);
|
||||||
|
if (!isCurrentSession() || callback == null) {
|
||||||
|
_disposeImage(image);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await callback(queued.display, image, isCurrentSession);
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
_disposeImage(image);
|
||||||
|
_onCallbackError(error, stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _QueuedFrame<Frame> {
|
||||||
|
const _QueuedFrame(this.display, this.frame, this.generation);
|
||||||
|
|
||||||
|
final int display;
|
||||||
|
final Frame frame;
|
||||||
|
final int generation;
|
||||||
|
}
|
||||||
@@ -12,3 +12,5 @@ Future<void> webSendLocalFiles(
|
|||||||
required bool isRemote}) {
|
required bool isRemote}) {
|
||||||
throw UnimplementedError("webSendLocalFiles");
|
throw UnimplementedError("webSendLocalFiles");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> loadLocalTerminalFontIfNeeded() async {}
|
||||||
|
|||||||
33
flutter/lib/web/terminal_font.dart
Normal file
33
flutter/lib/web/terminal_font.dart
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import 'dart:html' as html;
|
||||||
|
import 'dart:js' as js;
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
bool _loadRequested = false;
|
||||||
|
|
||||||
|
/// When Google CDNs are unreachable, `index.html` sets
|
||||||
|
/// `window.rustdeskLocalFonts` and `GoogleFonts.robotoMono()` cannot download
|
||||||
|
/// the terminal font. Load the copy bundled with the web app instead,
|
||||||
|
/// registered under the family name google_fonts gives the terminal's
|
||||||
|
/// TextStyle ('RobotoMono_regular').
|
||||||
|
Future<void> loadLocalTerminalFontIfNeeded() async {
|
||||||
|
if (_loadRequested || js.context['rustdeskLocalFonts'] != true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_loadRequested = true;
|
||||||
|
try {
|
||||||
|
final req = await html.HttpRequest.request(
|
||||||
|
'fonts/RobotoMono-Regular.ttf',
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
);
|
||||||
|
final data = ByteData.view(req.response as ByteBuffer);
|
||||||
|
final loader = FontLoader('RobotoMono_regular')
|
||||||
|
..addFont(Future.value(data));
|
||||||
|
await loader.load();
|
||||||
|
} catch (e) {
|
||||||
|
_loadRequested = false;
|
||||||
|
debugPrint('Failed to load bundled Roboto Mono: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user