init
This commit is contained in:
3
src/niceguiex/__init__.py
Normal file
3
src/niceguiex/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""NiceGUI Extensions - Extensions for NiceGUI with proper typing support"""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
10
src/niceguiex/async_elements/__init__.py
Normal file
10
src/niceguiex/async_elements/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from .base import AsyncElement
|
||||
from .elements import (AsyncColumn, AsyncRow, AsyncCard, AsyncScrollArea)
|
||||
|
||||
__all__ = [
|
||||
'AsyncElement',
|
||||
'AsyncColumn',
|
||||
'AsyncRow',
|
||||
'AsyncCard',
|
||||
'AsyncScrollArea'
|
||||
]
|
||||
47
src/niceguiex/async_elements/base.py
Normal file
47
src/niceguiex/async_elements/base.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Type, Any, TypeVar, Generic
|
||||
from nicegui import ui
|
||||
|
||||
# Type variable for the wrapped element type
|
||||
T = TypeVar('T', bound=ui.element)
|
||||
|
||||
|
||||
class AsyncElement(ABC, Generic[T]):
|
||||
"""Base class for UI elements with async initialization that behaves like a NiceGUI element"""
|
||||
|
||||
def __init__(self, element_type: Type[T] = ui.column, *element_args, **element_kwargs) -> None:
|
||||
# Create the NiceGUI element
|
||||
self._element: T = element_type(*element_args, **element_kwargs)
|
||||
|
||||
@abstractmethod
|
||||
async def build(self, *args, **kwargs) -> None:
|
||||
"""Build/setup the element - must be implemented by subclasses"""
|
||||
...
|
||||
|
||||
@classmethod
|
||||
async def create(cls, element_type: Type[T] = ui.column, *args, **kwargs) -> T:
|
||||
"""Factory method to create and build an element instance, returns the NiceGUI element"""
|
||||
# Separate element constructor args from build args
|
||||
element_args = kwargs.pop('element_args', ())
|
||||
element_kwargs = kwargs.pop('element_kwargs', {})
|
||||
|
||||
# Create and build the instance
|
||||
instance = cls(element_type, *element_args, **element_kwargs)
|
||||
await instance.build(*args, **kwargs)
|
||||
|
||||
# Add a reference to the async instance on the element for potential later access
|
||||
instance._element._async_instance = instance # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
return instance._element # Return the NiceGUI element with proper typing
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
"""Delegate any missing attribute access to the wrapped element"""
|
||||
return getattr(self._element, name)
|
||||
|
||||
def __enter__(self):
|
||||
"""Support context manager protocol"""
|
||||
return self._element.__enter__()
|
||||
|
||||
def __exit__(self, *args):
|
||||
"""Support context manager protocol"""
|
||||
return self._element.__exit__(*args)
|
||||
79
src/niceguiex/async_elements/elements.py
Normal file
79
src/niceguiex/async_elements/elements.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Self
|
||||
from nicegui import ui
|
||||
|
||||
|
||||
class AsyncColumn(ui.column, ABC):
|
||||
"""Async column that inherits from ui.column for perfect typing"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@abstractmethod
|
||||
async def build(self, *args, **kwargs) -> None:
|
||||
"""Build/setup the element - must be implemented by subclasses"""
|
||||
...
|
||||
|
||||
@classmethod
|
||||
async def create(cls, *args, **kwargs) -> Self:
|
||||
"""Factory method to create and build a column instance"""
|
||||
instance = cls()
|
||||
await instance.build(*args, **kwargs)
|
||||
return instance
|
||||
|
||||
|
||||
class AsyncRow(ui.row, ABC):
|
||||
"""Async row that inherits from ui.row for perfect typing"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@abstractmethod
|
||||
async def build(self, *args, **kwargs) -> None:
|
||||
"""Build/setup the element - must be implemented by subclasses"""
|
||||
...
|
||||
|
||||
@classmethod
|
||||
async def create(cls, *args, **kwargs) -> Self:
|
||||
"""Factory method to create and build a column instance"""
|
||||
instance = cls()
|
||||
await instance.build(*args, **kwargs)
|
||||
return instance
|
||||
|
||||
|
||||
class AsyncCard(ui.card, ABC):
|
||||
"""Async card that inherits from ui.card for perfect typing"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@abstractmethod
|
||||
async def build(self, *args, **kwargs) -> None:
|
||||
"""Build/setup the element - must be implemented by subclasses"""
|
||||
...
|
||||
|
||||
@classmethod
|
||||
async def create(cls, *args, **kwargs) -> Self:
|
||||
"""Factory method to create and build a card instance"""
|
||||
instance = cls()
|
||||
await instance.build(*args, **kwargs)
|
||||
return instance
|
||||
|
||||
|
||||
class AsyncScrollArea(ui.scroll_area, ABC):
|
||||
"""Async ScrollArea that inherits from ui.scrol_area for perfect typing"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@abstractmethod
|
||||
async def build(self, *args, **kwargs) -> None:
|
||||
"""Build/setup the element - must be implemented by subclasses"""
|
||||
...
|
||||
|
||||
@classmethod
|
||||
async def create(cls, *args, **kwargs) -> Self:
|
||||
"""Factory method to create and build a column instance"""
|
||||
instance = cls()
|
||||
await instance.build(*args, **kwargs)
|
||||
return instance
|
||||
4
src/niceguiex/components/__init__.py
Normal file
4
src/niceguiex/components/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .auto_scroll_area import AutoScrollArea
|
||||
from .chat_input import ChatInput
|
||||
|
||||
__all__ = ['AutoScrollArea', 'ChatInput']
|
||||
72
src/niceguiex/components/auto_scroll_area.py
Normal file
72
src/niceguiex/components/auto_scroll_area.py
Normal file
@@ -0,0 +1,72 @@
|
||||
from typing import Optional
|
||||
from nicegui import ui
|
||||
|
||||
|
||||
class AutoScrollArea(ui.scroll_area):
|
||||
"""A scroll area that automatically scrolls to bottom when new content is added
|
||||
|
||||
Features:
|
||||
- Auto-scrolls to bottom when at bottom and new content arrives
|
||||
- Stops auto-scroll when user scrolls up manually
|
||||
- Resumes auto-scroll when user scrolls back to bottom
|
||||
"""
|
||||
|
||||
_auto_scroll_enabled: bool = True
|
||||
_auto_scroll_timer: Optional[ui.timer] = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Set up scroll monitoring
|
||||
# self._handle_scroll
|
||||
self.on_scroll(self._handle_scroll_event)
|
||||
self.on('wheel', self._handle_wheel, ['deltaY'])
|
||||
|
||||
# Create timer for auto-scrolling
|
||||
self._auto_scroll_timer = ui.timer(0.1, lambda: self.scroll_to(percent=1))
|
||||
self._auto_scroll_timer.activate()
|
||||
|
||||
def _scroll_event_test(self, e):
|
||||
print(e.vertical_percentage)
|
||||
|
||||
def _handle_scroll_event(self, event_data):
|
||||
"""Handle scroll events to detect when user is at bottom"""
|
||||
|
||||
if not self._auto_scroll_timer:
|
||||
print('no timer instantiated.')
|
||||
return
|
||||
|
||||
# If scrolled to bottom (100%), enable auto-scroll
|
||||
if event_data.vertical_percentage > 0.99: # Using 0.99 for some tolerance
|
||||
if not self._auto_scroll_timer.active:
|
||||
self._auto_scroll_timer.activate()
|
||||
|
||||
def _handle_wheel(self, event_data):
|
||||
"""Handle mouse wheel events to detect manual scrolling"""
|
||||
delta_y = event_data.args['deltaY']
|
||||
if not self._auto_scroll_timer:
|
||||
print('no timer instantiated.')
|
||||
return
|
||||
# If scrolling up (negative delta), disable auto-scroll
|
||||
if delta_y < 0:
|
||||
if self._auto_scroll_timer.active:
|
||||
self._auto_scroll_timer.deactivate()
|
||||
|
||||
def enable_auto_scroll(self):
|
||||
"""Manually enable auto-scrolling"""
|
||||
if self._auto_scroll_timer:
|
||||
if not self._auto_scroll_timer.active:
|
||||
self._auto_scroll_timer.activate()
|
||||
|
||||
def disable_auto_scroll(self):
|
||||
"""Manually disable auto-scrolling"""
|
||||
|
||||
if self._auto_scroll_timer:
|
||||
if self._auto_scroll_timer.active:
|
||||
self._auto_scroll_timer.deactivate()
|
||||
|
||||
def cleanup(self):
|
||||
"""Clean up timer when component is destroyed"""
|
||||
if self._auto_scroll_timer:
|
||||
self._auto_scroll_timer.deactivate()
|
||||
self._auto_scroll_timer = None
|
||||
27
src/niceguiex/components/chat_input.py
Normal file
27
src/niceguiex/components/chat_input.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from typing import Optional, Callable, Awaitable
|
||||
|
||||
from nicegui import ui
|
||||
|
||||
|
||||
class ChatInput(ui.textarea):
|
||||
|
||||
def __init__(self,
|
||||
placeholder: str = 'Type your message...',
|
||||
on_enter: Optional[Callable[[str], Awaitable[None]]] = None,
|
||||
*args, **kwargs) -> None:
|
||||
super().__init__(placeholder=placeholder, *args, **kwargs)
|
||||
self._on_enter_callback = on_enter
|
||||
self.classes('flex-grow').props('outlined dense autogrow')
|
||||
self.on('keydown', self._handle_keydown)
|
||||
|
||||
async def _handle_keydown(self, event) -> None:
|
||||
"""Handle keyboard shortcuts for message input"""
|
||||
if hasattr(event, 'args') and event.args:
|
||||
key_event = event.args
|
||||
if key_event.get('key') == 'Enter' and not key_event.get('shiftKey', False):
|
||||
# Enter without Shift: Send message
|
||||
event.args['preventDefault'] = True
|
||||
if self._on_enter_callback and self.value:
|
||||
await self._on_enter_callback(self.value)
|
||||
self.value = ''
|
||||
# Shift+Enter: Allow default behavior (new line)
|
||||
Reference in New Issue
Block a user