Browse Source

#656 introduce ui.download

Falko Schindler 2 years ago
parent
commit
d18347bb6b

+ 14 - 4
nicegui/client.py

@@ -52,10 +52,12 @@ class Client:
 
 
     @property
     @property
     def ip(self) -> Optional[str]:
     def ip(self) -> Optional[str]:
+        """Return the IP address of the client, or None if the client is not connected."""
         return self.environ.get('REMOTE_ADDR') if self.environ else None
         return self.environ.get('REMOTE_ADDR') if self.environ else None
 
 
     @property
     @property
     def has_socket_connection(self) -> bool:
     def has_socket_connection(self) -> bool:
+        """Return True if the client is connected, False otherwise."""
         return self.environ is not None
         return self.environ is not None
 
 
     def __enter__(self):
     def __enter__(self):
@@ -88,7 +90,7 @@ class Client:
         }, status_code, {'Cache-Control': 'no-store', 'X-NiceGUI-Content': 'page'})
         }, status_code, {'Cache-Control': 'no-store', 'X-NiceGUI-Content': 'page'})
 
 
     async def connected(self, timeout: float = 3.0, check_interval: float = 0.1) -> None:
     async def connected(self, timeout: float = 3.0, check_interval: float = 0.1) -> None:
-        '''Blocks execution until the client is connected.'''
+        """Block execution until the client is connected."""
         self.is_waiting_for_connection = True
         self.is_waiting_for_connection = True
         deadline = time.time() + timeout
         deadline = time.time() + timeout
         while not self.environ:
         while not self.environ:
@@ -98,7 +100,7 @@ class Client:
         self.is_waiting_for_connection = False
         self.is_waiting_for_connection = False
 
 
     async def disconnected(self, check_interval: float = 0.1) -> None:
     async def disconnected(self, check_interval: float = 0.1) -> None:
-        '''Blocks execution until the client disconnects.'''
+        """Block execution until the client disconnects."""
         self.is_waiting_for_disconnect = True
         self.is_waiting_for_disconnect = True
         while self.id in globals.clients:
         while self.id in globals.clients:
             await asyncio.sleep(check_interval)
             await asyncio.sleep(check_interval)
@@ -106,11 +108,12 @@ class Client:
 
 
     async def run_javascript(self, code: str, *,
     async def run_javascript(self, code: str, *,
                              respond: bool = True, timeout: float = 1.0, check_interval: float = 0.01) -> Optional[str]:
                              respond: bool = True, timeout: float = 1.0, check_interval: float = 0.01) -> Optional[str]:
-        '''Allows execution of javascript on the client.
+        """Execute JavaScript on the client.
 
 
         The client connection must be established before this method is called.
         The client connection must be established before this method is called.
         You can do this by `await client.connected()` or register a callback with `client.on_connected(...)`.
         You can do this by `await client.connected()` or register a callback with `client.on_connected(...)`.
-        If respond is True, the javascript code must return a string.'''
+        If respond is True, the javascript code must return a string.
+        """
         request_id = str(uuid.uuid4())
         request_id = str(uuid.uuid4())
         command = {
         command = {
             'code': code,
             'code': code,
@@ -127,11 +130,18 @@ class Client:
         return self.waiting_javascript_commands.pop(request_id)
         return self.waiting_javascript_commands.pop(request_id)
 
 
     def open(self, target: Union[Callable, str]) -> None:
     def open(self, target: Union[Callable, str]) -> None:
+        """Open a new page in the client."""
         path = target if isinstance(target, str) else globals.page_routes[target]
         path = target if isinstance(target, str) else globals.page_routes[target]
         outbox.enqueue_message('open', path, self.id)
         outbox.enqueue_message('open', path, self.id)
 
 
+    def download(self, url: str, filename: Optional[str] = None) -> None:
+        """Download a file from the given URL."""
+        outbox.enqueue_message('download', {'url': url, 'filename': filename}, self.id)
+
     def on_connect(self, handler: Union[Callable, Awaitable]) -> None:
     def on_connect(self, handler: Union[Callable, Awaitable]) -> None:
+        """Register a callback to be called when the client connects."""
         self.connect_handlers.append(handler)
         self.connect_handlers.append(handler)
 
 
     def on_disconnect(self, handler: Union[Callable, Awaitable]) -> None:
     def on_disconnect(self, handler: Union[Callable, Awaitable]) -> None:
+        """Register a callback to be called when the client disconnects."""
         self.disconnect_handlers.append(handler)
         self.disconnect_handlers.append(handler)

+ 14 - 0
nicegui/functions/download.py

@@ -0,0 +1,14 @@
+from typing import Optional
+
+from .. import globals
+
+
+def download(url: str, filename: Optional[str] = None) -> None:
+    """Download
+
+    Function to trigger the download of a file.
+
+    :param url: target URL of the file to download
+    :param filename: name of the file to download (default: name of the file on the server)
+    """
+    globals.get_client().download(url, filename)

+ 11 - 0
nicegui/templates/index.html

@@ -130,6 +130,16 @@
         });
         });
       }
       }
 
 
+      function download(url, filename) {
+        const anchor = document.createElement("a");
+        anchor.href = url;
+        anchor.target = "_blank";
+        anchor.download = filename || "";
+        document.body.appendChild(anchor);
+        anchor.click();
+        document.body.removeChild(anchor);
+      }
+
       const app = Vue.createApp({
       const app = Vue.createApp({
         data() {
         data() {
           return {
           return {
@@ -162,6 +172,7 @@
           window.socket.on("run_method", (msg) => getElement(msg.id)?.[msg.name](...msg.args));
           window.socket.on("run_method", (msg) => getElement(msg.id)?.[msg.name](...msg.args));
           window.socket.on("run_javascript", (msg) => runJavascript(msg['code'], msg['request_id']));
           window.socket.on("run_javascript", (msg) => runJavascript(msg['code'], msg['request_id']));
           window.socket.on("open", (msg) => (location.href = msg));
           window.socket.on("open", (msg) => (location.href = msg));
+          window.socket.on("download", (msg) => download(msg.url, msg.filename));
           window.socket.on("notify", (msg) => Quasar.Notify.create(msg));
           window.socket.on("notify", (msg) => Quasar.Notify.create(msg));
         },
         },
       }).use(Quasar, {
       }).use(Quasar, {

+ 1 - 0
nicegui/ui.py

@@ -61,6 +61,7 @@ from .elements.tooltip import Tooltip as tooltip
 from .elements.tree import Tree as tree
 from .elements.tree import Tree as tree
 from .elements.upload import Upload as upload
 from .elements.upload import Upload as upload
 from .elements.video import Video as video
 from .elements.video import Video as video
+from .functions.download import download
 from .functions.html import add_body_html, add_head_html
 from .functions.html import add_body_html, add_head_html
 from .functions.javascript import run_javascript
 from .functions.javascript import run_javascript
 from .functions.notify import notify
 from .functions.notify import notify

+ 23 - 0
tests/test_download.py

@@ -0,0 +1,23 @@
+from fastapi import HTTPException
+
+from nicegui import app, ui
+
+from .screen import PORT, Screen
+
+
+def test_download(screen: Screen):
+    success = False
+
+    @app.get('/static/test.py')
+    def test():
+        nonlocal success
+        success = True
+        raise HTTPException(404, 'Not found')
+
+    ui.button('Download', on_click=lambda: ui.download('static/test.py'))
+
+    screen.open('/')
+    screen.click('Download')
+    screen.wait(0.5)
+    assert success
+    screen.assert_py_logger('WARNING', f'http://localhost:{PORT}/static/test.py not found')

+ 1 - 0
website/documentation.py

@@ -410,6 +410,7 @@ def create_full() -> None:
         ui.link('show page with fancy layout', page_layout)
         ui.link('show page with fancy layout', page_layout)
 
 
     load_demo(ui.open)
     load_demo(ui.open)
+    load_demo(ui.download)
 
 
     @text_demo('Sessions', '''
     @text_demo('Sessions', '''
         The optional `request` argument provides insights about the client's URL parameters etc.
         The optional `request` argument provides insights about the client's URL parameters etc.

+ 5 - 0
website/more_documentation/download_documentation.py

@@ -0,0 +1,5 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    ui.button('NiceGUI Logo', on_click=lambda: ui.download('https://nicegui.io/logo.png'))