diff --git a/.fvmrc b/.fvmrc index 6a1a5084a1..1d524b394f 100644 --- a/.fvmrc +++ b/.fvmrc @@ -1,3 +1,3 @@ { - "flutter": "3.38.6" + "flutter": "3.38.7" } diff --git a/.github/scripts/clean-pypi.sh b/.github/scripts/clean-pypi.sh index d93036d6cf..dec2631719 100644 --- a/.github/scripts/clean-pypi.sh +++ b/.github/scripts/clean-pypi.sh @@ -1,6 +1,6 @@ # set PYPI_CLEANUP_PASSWORD with pypi.org password -VER="0\.81\.0\.dev(?!7287)" +VER="0\.81\.0\.dev(?!7200)" uv tool install pypi-cleanup uvx pypi-cleanup -u flet -p flet -y -r $VER --do-it uvx pypi-cleanup -u flet -p flet-cli -y -r $VER --do-it diff --git a/.github/scripts/update_build_version.sh b/.github/scripts/update_build_version.sh index 504b974a8a..3ef5a69cd1 100755 --- a/.github/scripts/update_build_version.sh +++ b/.github/scripts/update_build_version.sh @@ -41,16 +41,16 @@ else # Remove leading "v" if present cv=${cv#v} - # Split into major/minor components + # Split into major/minor/patch components major=$(echo "$cv" | cut -d. -f1) minor=$(echo "$cv" | cut -d. -f2) + patch=$(echo "$cv" | cut -d. -f3) - # Increment the minor version (e.g. "1.2" → "1.3") - minor=$((minor + 1)) - - # Construct the package version: ..0 - export PKG_VER="${major}.${minor}.0" + # Increment patch version (e.g. "1.2.3" → "1.2.4") + patch=$((patch + 1)) + # Construct the package version: .. + export PKG_VER="${major}.${minor}.${patch}" # PyPI build version: + export PYPI_VER="${PKG_VER}.dev${BUILD_NUM}" fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 682cf5a539..795302628f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.80.4 + +* fix: Enable TextButton style and full-width Dropdown ([#6048](https://github.com/flet-dev/flet/issues/6048)). +* flet-video: add mpv_properties to VideoConfiguration ([#6041](https://github.com/flet-dev/flet/issues/6041)). +* Refactor `Icons` and `CupertinoIcons` proxies for member caching and iteration ([#6055](https://github.com/flet-dev/flet/issues/6055)). +* Flutter 3.38.7. + ## 0.80.3 * Lazy loading of icons, theme for faster app startup ([#6043](https://github.com/flet-dev/flet/issues/6043)). diff --git a/client/pubspec.lock b/client/pubspec.lock index 9bec460f66..4276781ccf 100644 --- a/client/pubspec.lock +++ b/client/pubspec.lock @@ -295,7 +295,7 @@ packages: path: "../packages/flet" relative: true source: path - version: "0.80.3" + version: "0.80.4" flet_ads: dependency: "direct main" description: diff --git a/packages/flet/CHANGELOG.md b/packages/flet/CHANGELOG.md index 098e0427a0..1516a63a16 100644 --- a/packages/flet/CHANGELOG.md +++ b/packages/flet/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.80.4 + +* fix: Enable TextButton style and full-width Dropdown ([#6048](https://github.com/flet-dev/flet/issues/6048)). +* flet-video: add mpv_properties to VideoConfiguration ([#6041](https://github.com/flet-dev/flet/issues/6041)). +* Refactor `Icons` and `CupertinoIcons` proxies for member caching and iteration ([#6055](https://github.com/flet-dev/flet/issues/6055)). +* Flutter 3.38.7. + ## 0.80.3 * feat: add `locale` prop to `CupertinoDatePicker`, `DatePicker`, `DateRangePicker`, `TimePicker` ([#6030](https://github.com/flet-dev/flet/issues/6030)). diff --git a/packages/flet/lib/src/controls/button.dart b/packages/flet/lib/src/controls/button.dart index c154ec97e7..3d2b276ad9 100644 --- a/packages/flet/lib/src/controls/button.dart +++ b/packages/flet/lib/src/controls/button.dart @@ -98,7 +98,7 @@ class _ButtonControlState extends State with FletStoreMixin { var theme = Theme.of(context); var style = parseButtonStyle( - widget.control.internals?["style"], + widget.control.internals?["style"] ?? widget.control.get("style"), theme, defaultForegroundColor: widget.control.getColor("color", context, theme.colorScheme.primary)!, diff --git a/packages/flet/lib/src/controls/dropdown.dart b/packages/flet/lib/src/controls/dropdown.dart index dc472f1d63..abaa6511ac 100644 --- a/packages/flet/lib/src/controls/dropdown.dart +++ b/packages/flet/lib/src/controls/dropdown.dart @@ -10,6 +10,7 @@ import '../utils/buttons.dart'; import '../utils/colors.dart'; import '../utils/edge_insets.dart'; import '../utils/form_field.dart'; +import '../utils/layout.dart'; import '../utils/menu.dart'; import '../utils/numbers.dart'; import '../utils/text.dart'; @@ -270,6 +271,9 @@ class _DropdownControlState extends State { _focusNode.canRequestFocus = editable; + int expand = widget.control.getExpand("expand", 0)!; + EdgeInsets? expandedInsets = expand > 0 ? EdgeInsets.zero : null; + Widget dropDown = DropdownMenu( enabled: !widget.control.disabled, focusNode: _focusNode, @@ -291,6 +295,7 @@ class _DropdownControlState extends State { hintText: widget.control.getString("hint_text"), helperText: widget.control.getString("helper_text"), menuStyle: menuStyle, + expandedInsets: expandedInsets, inputDecorationTheme: inputDecorationTheme, inputFormatters: inputFormatters.isEmpty ? null : inputFormatters, onSelected: widget.control.disabled diff --git a/packages/flet/pubspec.yaml b/packages/flet/pubspec.yaml index 2932fcc9ff..ae078d3b06 100644 --- a/packages/flet/pubspec.yaml +++ b/packages/flet/pubspec.yaml @@ -2,7 +2,7 @@ name: flet description: Write entire Flutter app in Python or add server-driven UI experience into existing Flutter app. homepage: https://flet.dev repository: https://github.com/flet-dev/flet/tree/main/packages/flet -version: 0.80.3 +version: 0.80.4 # Supported platforms platforms: diff --git a/sdk/python/packages/flet-video/src/flet_video/types.py b/sdk/python/packages/flet-video/src/flet_video/types.py index 77db740dec..f25308ec09 100644 --- a/sdk/python/packages/flet-video/src/flet_video/types.py +++ b/sdk/python/packages/flet-video/src/flet_video/types.py @@ -4,7 +4,7 @@ from dataclasses import dataclass, field from enum import Enum -from typing import Optional +from typing import Optional, Union import flet as ft @@ -90,6 +90,29 @@ class VideoConfiguration: Specifying this option will cause [`width`][(c).] & [`height`][(c).] to be ignored. """ + mpv_properties: Optional[dict[str, Union[str, int, float, bool]]] = None + """ + Extra mpv/libmpv properties to set on + native backends (Windows/macOS/Linux/iOS/Android). + + The keys are mpv option/property names without the leading `--`. Values can be + `str`, `int`, `float` or `bool`. All values are converted to strings before being + passed to mpv; boolean values are converted to `"yes"` / `"no"`. + + Full list of mpv options: https://mpv.io/manual/stable/#options + + Example: + ```python + >>> VideoConfiguration( + mpv_properties={ + "profile": "low-latency", # --profile=low-latency + "untimed": True, # --untimed + "volume": 80, # --volume=80 + } + ) + ``` + """ + @dataclass class VideoSubtitleTrack: diff --git a/sdk/python/packages/flet-video/src/flutter/flet_video/lib/src/video.dart b/sdk/python/packages/flet-video/src/flutter/flet_video/lib/src/video.dart index bde5ab167c..535b76bd46 100644 --- a/sdk/python/packages/flet-video/src/flutter/flet_video/lib/src/video.dart +++ b/sdk/python/packages/flet-video/src/flutter/flet_video/lib/src/video.dart @@ -25,6 +25,27 @@ class _VideoControlState extends State with FletStoreMixin { late VideoController _controller; bool _initialized = false; + Future _applyMpvProperties(Control control) async { + final cfg = control.get("configuration"); + if (cfg is! Map) return; + + final mpvPropsRaw = cfg["mpv_properties"]; + if (mpvPropsRaw is! Map) return; + + final platform = _player.platform; + if (platform is! NativePlayer) return; + final native = platform as dynamic; + + for (final entry in mpvPropsRaw.entries) { + final key = entry.key.toString(); + final val = entry.value; + if (val == null) continue; + final valueStr = val is bool ? (val ? "yes" : "no") : val.toString(); + await native.setProperty(key, valueStr); + } + } + + void _setup(Control control) { final playerConfig = PlayerConfiguration( title: control.getString("title", "flet-video")!, @@ -64,8 +85,14 @@ class _VideoControlState extends State with FletStoreMixin { }); } - _player.open(Playlist(parseVideoMedias(control.get("playlist"), [])!), - play: control.getBool("autoplay", false)!); + final playlist = + Playlist(parseVideoMedias(control.get("playlist"), [])!); + final autoplay = control.getBool("autoplay", false)!; + + () async { + await _applyMpvProperties(control); + await _player.open(playlist, play: autoplay); + }(); } void _teardown(Control control) { @@ -294,4 +321,4 @@ class _VideoControlState extends State with FletStoreMixin { return ConstrainedControl(control: widget.control, child: video); } -} +} \ No newline at end of file diff --git a/sdk/python/packages/flet/src/flet/controls/cupertino/cupertino_icons.py b/sdk/python/packages/flet/src/flet/controls/cupertino/cupertino_icons.py index 44a65a7087..642e8d285b 100644 --- a/sdk/python/packages/flet/src/flet/controls/cupertino/cupertino_icons.py +++ b/sdk/python/packages/flet/src/flet/controls/cupertino/cupertino_icons.py @@ -21,6 +21,10 @@ def _missing_(cls, value): return obj +_CupertinoIconData.__name__ = "CupertinoIcons" +_CupertinoIconData.__qualname__ = "CupertinoIcons" + + class _CupertinoIconsProxy: __slots__ = ("_map", "_values") @@ -38,18 +42,32 @@ def _load(self) -> None: ) self._map = json.loads(data) + def _get_member(self, name: str) -> _CupertinoIconData: + self._load() + assert self._map is not None + cls = _CupertinoIconData + existing = cls._member_map_.get(name) + if existing is not None: + return existing + value = self._map[name] + member = int.__new__(cls, value) + member._value_ = value + member._name_ = name + cls._member_map_[name] = member + cls._value2member_map_[value] = member + cls._member_names_.append(name) + return member + def _get_values(self) -> list[_CupertinoIconData]: self._load() if self._values is None: assert self._map is not None - self._values = [_CupertinoIconData(v) for v in self._map.values()] + self._values = [self._get_member(name) for name in self._map] return self._values def __getattr__(self, name: str) -> IconData: - self._load() - assert self._map is not None try: - return _CupertinoIconData(self._map[name]) + return self._get_member(name) except KeyError: raise AttributeError(name) from None @@ -58,6 +76,14 @@ def __dir__(self) -> list[str]: assert self._map is not None return sorted(self._map.keys()) + def __iter__(self): + return iter(self._get_values()) + + def __len__(self) -> int: + self._load() + assert self._map is not None + return len(self._map) + def random( self, exclude: list[IconData] | None = None, diff --git a/sdk/python/packages/flet/src/flet/controls/material/icons.py b/sdk/python/packages/flet/src/flet/controls/material/icons.py index ffc89ae912..88c326fb1a 100644 --- a/sdk/python/packages/flet/src/flet/controls/material/icons.py +++ b/sdk/python/packages/flet/src/flet/controls/material/icons.py @@ -21,6 +21,10 @@ def _missing_(cls, value): return obj +_MaterialIconData.__name__ = "Icons" +_MaterialIconData.__qualname__ = "Icons" + + class _IconsProxy: __slots__ = ("_map", "_values") @@ -38,18 +42,32 @@ def _load(self) -> None: ) self._map = json.loads(data) + def _get_member(self, name: str) -> _MaterialIconData: + self._load() + assert self._map is not None + cls = _MaterialIconData + existing = cls._member_map_.get(name) + if existing is not None: + return existing + value = self._map[name] + member = int.__new__(cls, value) + member._value_ = value + member._name_ = name + cls._member_map_[name] = member + cls._value2member_map_[value] = member + cls._member_names_.append(name) + return member + def _get_values(self) -> list[_MaterialIconData]: self._load() if self._values is None: assert self._map is not None - self._values = [_MaterialIconData(v) for v in self._map.values()] + self._values = [self._get_member(name) for name in self._map] return self._values def __getattr__(self, name: str) -> IconData: - self._load() - assert self._map is not None try: - return _MaterialIconData(self._map[name]) + return self._get_member(name) except KeyError: raise AttributeError(name) from None @@ -58,6 +76,14 @@ def __dir__(self) -> list[str]: assert self._map is not None return sorted(self._map.keys()) + def __iter__(self): + return iter(self._get_values()) + + def __len__(self) -> int: + self._load() + assert self._map is not None + return len(self._map) + def random( self, exclude: list[IconData] | None = None,