diff --git a/client/lib/controls/image.dart b/client/lib/controls/image.dart index c61700d27..e7984bed2 100644 --- a/client/lib/controls/image.dart +++ b/client/lib/controls/image.dart @@ -46,7 +46,7 @@ class ImageControl extends StatelessWidget { builder: (context, pageUri) { return baseControl( _clipCorners( - Image.network(getAssetUrl(pageUri!, src), + Image.network(getAssetUri(pageUri!, src).toString(), width: width, height: height, repeat: repeat, fit: fit), control), parent, diff --git a/client/lib/controls/page.dart b/client/lib/controls/page.dart index f192002ff..c1100c6ee 100644 --- a/client/lib/controls/page.dart +++ b/client/lib/controls/page.dart @@ -1,3 +1,4 @@ +import 'package:flet_view/utils/user_fonts.dart'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; @@ -11,6 +12,7 @@ import '../utils/colors.dart'; import '../utils/desktop.dart'; import '../utils/edge_insets.dart'; import '../utils/theme.dart'; +import '../utils/uri.dart'; import '../widgets/page_media.dart'; import 'app_bar.dart'; import 'create_control.dart'; @@ -110,89 +112,109 @@ class PageControl extends StatelessWidget { childIds.add(appBar.id); } - return StoreConnector( + return StoreConnector( distinct: true, - converter: (store) => PageMediaViewModel.fromStore(store), - builder: (context, media) { - var theme = themeMode == ThemeMode.light || - (themeMode == ThemeMode.system && - media.displayBrightness == Brightness.light) - ? lightTheme - : darkTheme; - - return StoreConnector( + converter: (store) => store.state.pageUri, + builder: (context, pageUri) { + // load custom fonts + parseFonts(control, "fonts").forEach((fontFamily, fontUrl) { + var fontUri = Uri.parse(fontUrl); + if (!fontUri.hasAuthority) { + fontUri = getAssetUri(pageUri!, fontUrl); + } + debugPrint("fontUri: $fontUri"); + UserFonts.loadFont(fontFamily, fontUri); + }); + + return StoreConnector( distinct: true, - converter: (store) => - ControlsViewModel.fromStore(store, childIds), - builder: (context, childrenViews) { - debugPrint("Offstage StoreConnector build"); - - // offstage - List offstageWidgets = offstage != null - ? childrenViews.controlViews.first.children - .where((c) => - c.isVisible && - c.type != ControlType.floatingActionButton) - .map((c) => createControl(offstage, c.id, disabled)) - .toList() - : []; - - List fab = offstage != null - ? childrenViews.controlViews.first.children - .where((c) => - c.isVisible && - c.type == ControlType.floatingActionButton) - .toList() - : []; - - var appBarView = - appBar != null ? childrenViews.controlViews.last : null; - - var column = Column( - mainAxisAlignment: mainAlignment, - crossAxisAlignment: crossAlignment, - children: controls); - - return MaterialApp( - title: title, - theme: lightTheme, - darkTheme: darkTheme, - themeMode: themeMode, - home: Scaffold( - appBar: appBarView != null - ? AppBarControl( - parent: control, - control: appBarView.control, - children: appBarView.children, - parentDisabled: disabled, - height: appBarView.control - .attrDouble("toolbarHeight", kToolbarHeight)!, - theme: theme) - : null, - body: Stack(children: [ - SizedBox.expand( - child: Container( - padding: parseEdgeInsets(control, "padding") ?? - const EdgeInsets.all(10), - decoration: BoxDecoration( - color: HexColor.fromString(theme, - control.attrString("bgcolor", "")!)), - child: scrollMode != ScrollMode.none - ? ScrollableControl( - child: column, - scrollDirection: Axis.vertical, - scrollMode: scrollMode, - autoScroll: autoScroll, - ) - : column)), - ...offstageWidgets, - const PageMedia() - ]), - floatingActionButton: fab.isNotEmpty - ? createControl(offstage, fab.first.id, disabled) - : null, - ), - ); + converter: (store) => PageMediaViewModel.fromStore(store), + builder: (context, media) { + var theme = themeMode == ThemeMode.light || + (themeMode == ThemeMode.system && + media.displayBrightness == Brightness.light) + ? lightTheme + : darkTheme; + + return StoreConnector( + distinct: true, + converter: (store) => + ControlsViewModel.fromStore(store, childIds), + builder: (context, childrenViews) { + debugPrint("Offstage StoreConnector build"); + + // offstage + List offstageWidgets = offstage != null + ? childrenViews.controlViews.first.children + .where((c) => + c.isVisible && + c.type != ControlType.floatingActionButton) + .map((c) => + createControl(offstage, c.id, disabled)) + .toList() + : []; + + List fab = offstage != null + ? childrenViews.controlViews.first.children + .where((c) => + c.isVisible && + c.type == ControlType.floatingActionButton) + .toList() + : []; + + var appBarView = appBar != null + ? childrenViews.controlViews.last + : null; + + var column = Column( + mainAxisAlignment: mainAlignment, + crossAxisAlignment: crossAlignment, + children: controls); + + return MaterialApp( + title: title, + theme: lightTheme, + darkTheme: darkTheme, + themeMode: themeMode, + home: Scaffold( + appBar: appBarView != null + ? AppBarControl( + parent: control, + control: appBarView.control, + children: appBarView.children, + parentDisabled: disabled, + height: appBarView.control.attrDouble( + "toolbarHeight", kToolbarHeight)!, + theme: theme) + : null, + body: Stack(children: [ + SizedBox.expand( + child: Container( + padding: + parseEdgeInsets(control, "padding") ?? + const EdgeInsets.all(10), + decoration: BoxDecoration( + color: HexColor.fromString( + theme, + control.attrString( + "bgcolor", "")!)), + child: scrollMode != ScrollMode.none + ? ScrollableControl( + child: column, + scrollDirection: Axis.vertical, + scrollMode: scrollMode, + autoScroll: autoScroll, + ) + : column)), + ...offstageWidgets, + const PageMedia() + ]), + floatingActionButton: fab.isNotEmpty + ? createControl(offstage, fab.first.id, disabled) + : null, + ), + ); + }); }); }); } diff --git a/client/lib/controls/text.dart b/client/lib/controls/text.dart index 4c6171091..6795fdbdd 100644 --- a/client/lib/controls/text.dart +++ b/client/lib/controls/text.dart @@ -30,6 +30,7 @@ class TextControl extends StatelessWidget { fontSize: control.attrDouble("size", null), fontWeight: getFontWeight(control.attrString("weight", "")!), fontStyle: control.attrBool("italic", false)! ? FontStyle.italic : null, + fontFamily: control.attrString("fontFamily"), color: HexColor.fromString( Theme.of(context), control.attrString("color", "")!), backgroundColor: HexColor.fromString( diff --git a/client/lib/utils/theme.dart b/client/lib/utils/theme.dart index 8d4be394c..bfefa0735 100644 --- a/client/lib/utils/theme.dart +++ b/client/lib/utils/theme.dart @@ -20,6 +20,8 @@ ThemeData themeFromJson(Map json) { brightness: Brightness.values.firstWhere( (b) => b.name.toLowerCase() == json["brightness"], orElse: () => Brightness.light), - colorSchemeSeed: HexColor.fromString(null, json["color_scheme_seed"]), + colorSchemeSeed: + HexColor.fromString(null, json["color_scheme_seed"] ?? ""), + fontFamily: json["font_family"], useMaterial3: json["use_material3"]); } diff --git a/client/lib/utils/uri.dart b/client/lib/utils/uri.dart index 75af6ba35..9eacd3064 100644 --- a/client/lib/utils/uri.dart +++ b/client/lib/utils/uri.dart @@ -9,11 +9,10 @@ String getWebSocketEndpoint(Uri uri) { return "$wsScheme://${uri.authority}/ws"; } -String getAssetUrl(Uri pageUri, String assetPath) { +Uri getAssetUri(Uri pageUri, String assetPath) { return Uri( - scheme: pageUri.scheme, - host: pageUri.host, - port: pageUri.port, - path: assetPath) - .toString(); + scheme: pageUri.scheme, + host: pageUri.host, + port: pageUri.port, + path: assetPath); } diff --git a/client/lib/utils/user_fonts.dart b/client/lib/utils/user_fonts.dart new file mode 100644 index 000000000..9153038bd --- /dev/null +++ b/client/lib/utils/user_fonts.dart @@ -0,0 +1,46 @@ +import 'dart:convert'; + +import 'package:flutter/services.dart'; +import 'package:http/http.dart' as http; + +import '../models/control.dart'; + +class UserFonts { + static Map fontLoaders = {}; + + static void loadFont(String fontFamily, Uri fontUri) { + var key = "$fontFamily$fontUri"; + if (fontLoaders.containsKey(key)) { + return; + } + var fontLoader = FontLoader(fontFamily); + fontLoaders[key] = fontLoader; + fontLoader.addFont(fetchFont(fontUri)); + fontLoader.load(); + } + + static Future fetchFont(Uri uri) async { + final response = await http.get(uri); + + if (response.statusCode == 200) { + return ByteData.view(response.bodyBytes.buffer); + } else { + // If that call was not successful, throw an error. + throw Exception('Failed to load font $uri'); + } + } +} + +Map parseFonts(Control control, String propName) { + var v = control.attrString(propName, null); + if (v == null) { + return {}; + } + + final j1 = json.decode(v); + return fontsFromJson(j1); +} + +Map fontsFromJson(Map json) { + return json.map((key, value) => MapEntry(key, value)); +} diff --git a/client/pubspec.lock b/client/pubspec.lock index d402a38db..a3aa19481 100644 --- a/client/pubspec.lock +++ b/client/pubspec.lock @@ -116,6 +116,20 @@ packages: description: flutter source: sdk version: "0.0.0" + http: + dependency: "direct main" + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.4" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.1" image: dependency: transitive description: diff --git a/client/pubspec.yaml b/client/pubspec.yaml index 0119be97c..c0f2b3e78 100644 --- a/client/pubspec.yaml +++ b/client/pubspec.yaml @@ -38,6 +38,7 @@ dependencies: equatable: ^2.0.3 web_socket_channel: ^2.1.0 window_manager: ^0.2.1 + http: ^0.13.3 dev_dependencies: flutter_test: diff --git a/client/test/utils/user_fonts_test.dart b/client/test/utils/user_fonts_test.dart new file mode 100644 index 000000000..9ee46c516 --- /dev/null +++ b/client/test/utils/user_fonts_test.dart @@ -0,0 +1,20 @@ +import 'dart:convert'; + +import 'package:flet_view/utils/user_fonts.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test("Custom fonts are parsed from JSON", () { + const t1 = '''{ + "font1": "https://fonts.com/font1.ttf", + "font2": "https://fonts.com/font2.ttf" + }'''; + + final j1 = json.decode(t1); + var fonts = fontsFromJson(j1); + + expect(fonts.length, 2); + expect(fonts["font1"], "https://fonts.com/font1.ttf"); + expect(fonts["font2"], "https://fonts.com/font2.ttf"); + }); +} diff --git a/sdk/python/flet/container.py b/sdk/python/flet/container.py index b10768160..64be03c4e 100644 --- a/sdk/python/flet/container.py +++ b/sdk/python/flet/container.py @@ -7,7 +7,13 @@ from flet.border import Border from flet.border_radius import BorderRadius from flet.constrained_control import ConstrainedControl -from flet.control import BorderStyle, Control, MarginValue, OptionalNumber, PaddingValue +from flet.control import ( + BorderRadiusValue, + Control, + MarginValue, + OptionalNumber, + PaddingValue, +) from flet.ref import Ref try: @@ -37,7 +43,7 @@ def __init__( alignment: Alignment = None, bgcolor: str = None, border: Border = None, - border_radius: BorderRadius = None, + border_radius: BorderRadiusValue = None, ): ConstrainedControl.__init__( self, @@ -134,7 +140,7 @@ def border_radius(self): @border_radius.setter @beartype - def border_radius(self, value: Optional[BorderRadius]): + def border_radius(self, value: BorderRadiusValue): self.__border_radius = value if value and isinstance(value, (int, float)): value = border_radius.all(value) diff --git a/sdk/python/flet/control.py b/sdk/python/flet/control.py index b1dd51173..151c0db6b 100644 --- a/sdk/python/flet/control.py +++ b/sdk/python/flet/control.py @@ -7,6 +7,7 @@ from beartype import beartype from beartype.typing import List, Optional +from flet.border_radius import BorderRadius from flet.embed_json_encoder import EmbedJsonEncoder from flet.margin import Margin from flet.padding import Padding @@ -53,6 +54,8 @@ MarginValue = Union[None, int, float, Margin] +BorderRadiusValue = Union[None, int, float, BorderRadius] + ScrollMode = Literal[None, True, False, "none", "auto", "adaptive", "always"] diff --git a/sdk/python/flet/image.py b/sdk/python/flet/image.py index e6e06ad1f..a43ff24bb 100644 --- a/sdk/python/flet/image.py +++ b/sdk/python/flet/image.py @@ -4,7 +4,7 @@ from flet import border_radius from flet.border_radius import BorderRadius -from flet.control import Control, OptionalNumber +from flet.control import BorderRadiusValue, Control, OptionalNumber from flet.ref import Ref try: @@ -38,7 +38,7 @@ def __init__( src: str = None, repeat: ImageRepeat = None, fit: ImageFit = None, - border_radius: BorderRadius = None, + border_radius: BorderRadiusValue = None, ): Control.__init__( @@ -118,7 +118,7 @@ def border_radius(self): @border_radius.setter @beartype - def border_radius(self, value: Optional[BorderRadius]): + def border_radius(self, value: BorderRadiusValue): self.__border_radius = value if value and isinstance(value, (int, float)): value = border_radius.all(value) diff --git a/sdk/python/flet/page.py b/sdk/python/flet/page.py index c47f64ca7..14bb90f78 100644 --- a/sdk/python/flet/page.py +++ b/sdk/python/flet/page.py @@ -1,10 +1,9 @@ import json import logging import threading -from typing import Union from beartype import beartype -from beartype.typing import List, Optional +from beartype.typing import Dict, List, Optional from flet import constants, padding from flet.app_bar import AppBar @@ -51,6 +50,7 @@ def __init__(self, conn: Connection, session_id): self._event_available = threading.Event() self._fetch_page_details() + self.__fonts: Dict[str, str] = None self.__offstage = Offstage() self.__appbar = None self.__theme = None @@ -330,6 +330,17 @@ def design(self): def design(self, value: PageDesign): self._set_attr("design", value) + # fonts + @property + def fonts(self): + return self.__fonts + + @fonts.setter + @beartype + def fonts(self, value: Optional[Dict[str, str]]): + self.__fonts = value + self._set_attr_json("fonts", value) + # clipboard @property def clipboard(self): diff --git a/sdk/python/flet/text.py b/sdk/python/flet/text.py index 9564b93d5..2cbe8b935 100644 --- a/sdk/python/flet/text.py +++ b/sdk/python/flet/text.py @@ -46,6 +46,7 @@ def __init__( # text-specific # text_align: TextAlign = None, + font_family: str = None, size: OptionalNumber = None, weight: FontWeight = None, italic: bool = None, @@ -73,6 +74,7 @@ def __init__( self.value = value self.text_align = text_align + self.font_family = font_family self.size = size self.weight = weight self.italic = italic @@ -106,6 +108,15 @@ def text_align(self): def text_align(self, value: TextAlign): self._set_attr("textAlign", value) + # font_family + @property + def font_family(self): + return self._get_attr("fontFamily") + + @font_family.setter + def font_family(self, value): + self._set_attr("fontFamily", value) + # size @property def size(self): diff --git a/sdk/python/flet/theme.py b/sdk/python/flet/theme.py index 614737c3a..f90605844 100644 --- a/sdk/python/flet/theme.py +++ b/sdk/python/flet/theme.py @@ -11,4 +11,5 @@ class Theme: color_scheme_seed: str = field(default=None) brightness: Literal[None, "dark", "light"] = field(default="light") + font_family: str = field(default=None) use_material3: bool = field(default=False)