Преглед изворни кода

Merge pull request #808 from zauberzeug/favicon

Allow base64 and SVG Favicons
Falko Schindler пре 2 година
родитељ
комит
d9f2a24f40

+ 19 - 4
nicegui/favicon.py

@@ -1,4 +1,4 @@
-import urllib
+import urllib.parse
 from pathlib import Path
 from typing import TYPE_CHECKING, Optional
 
@@ -25,7 +25,11 @@ def get_favicon_url(page: 'page', prefix: str) -> str:
         return favicon
     elif not favicon:
         return f'{prefix}/_nicegui/{__version__}/static/favicon.ico'
-    if is_char(favicon):
+    elif is_data_url(favicon):
+        return favicon
+    elif is_svg(favicon):
+        return svg_to_data_url(favicon)
+    elif is_char(favicon):
         return char_to_data_url(favicon)
     elif page.path == '/':
         return f'{prefix}/favicon.ico'
@@ -41,6 +45,14 @@ def is_char(favicon: str) -> bool:
     return len(favicon) == 1
 
 
+def is_svg(favicon: str) -> bool:
+    return favicon.strip().startswith('<svg')
+
+
+def is_data_url(favicon: str) -> bool:
+    return favicon.startswith('data:')
+
+
 def char_to_data_url(char: str) -> str:
     svg = f'''
         <svg viewBox="0 0 128 128" width="128" height="128" xmlns="http://www.w3.org/2000/svg" >
@@ -58,6 +70,9 @@ def char_to_data_url(char: str) -> str:
             <text y=".9em" font-size="128" font-family="Georgia, sans-serif">{char}</text>
         </svg>
     '''
+    return svg_to_data_url(svg)
+
+
+def svg_to_data_url(svg: str) -> str:
     svg_urlencoded = urllib.parse.quote(svg)
-    data_url = f"data:image/svg+xml,{svg_urlencoded}"
-    return data_url
+    return f'data:image/svg+xml,{svg_urlencoded}'

+ 35 - 35
website/demo.py

@@ -1,5 +1,5 @@
 import inspect
-from typing import Callable, Optional
+from typing import Callable, Optional, Union
 
 import isort
 
@@ -19,37 +19,32 @@ def remove_prefix(text: str, prefix: str) -> str:
     return text[len(prefix):] if text.startswith(prefix) else text
 
 
-class demo:
-
-    def __init__(self, browser_title: Optional[str] = None) -> None:
-        self.browser_title = browser_title
-
-    def __call__(self, f: Callable) -> Callable:
-        with ui.column().classes('w-full items-stretch gap-8 no-wrap min-[1500px]:flex-row'):
-            code = inspect.getsource(f).split('# END OF DEMO')[0].strip().splitlines()
-            while not code[0].strip().startswith('def') and not code[0].strip().startswith('async def'):
-                del code[0]
+def demo(f: Callable) -> Callable:
+    with ui.column().classes('w-full items-stretch gap-8 no-wrap min-[1500px]:flex-row'):
+        code = inspect.getsource(f).split('# END OF DEMO')[0].strip().splitlines()
+        while not code[0].strip().startswith('def') and not code[0].strip().startswith('async def'):
             del code[0]
-            indentation = len(code[0]) - len(code[0].lstrip())
-            code = [line[indentation:] for line in code]
-            code = ['from nicegui import ui'] + [remove_prefix(line, '# ') for line in code]
-            code = ['' if line == '#' else line for line in code]
-            if not code[-1].startswith('ui.run('):
-                code.append('')
-                code.append('ui.run()')
-            code = isort.code('\n'.join(code), no_sections=True, lines_after_imports=1)
-            with python_window(classes='w-full max-w-[44rem]'):
-                async def copy_code():
-                    await ui.run_javascript('navigator.clipboard.writeText(`' + code + '`)', respond=False)
-                    ui.notify('Copied to clipboard', type='positive', color='primary')
-                ui.markdown(f'````python\n{code}\n````')
-                ui.icon('content_copy', size='xs') \
-                    .classes('absolute right-2 top-10 opacity-10 hover:opacity-80 cursor-pointer') \
-                    .on('click', copy_code)
-            with browser_window(self.browser_title,
-                                classes='w-full max-w-[44rem] min-[1500px]:max-w-[20rem] min-h-[10rem] browser-window'):
-                intersection_observer(on_intersection=f)
-        return f
+        del code[0]
+        indentation = len(code[0]) - len(code[0].lstrip())
+        code = [line[indentation:] for line in code]
+        code = ['from nicegui import ui'] + [remove_prefix(line, '# ') for line in code]
+        code = ['' if line == '#' else line for line in code]
+        if not code[-1].startswith('ui.run('):
+            code.append('')
+            code.append('ui.run()')
+        code = isort.code('\n'.join(code), no_sections=True, lines_after_imports=1)
+        with python_window(classes='w-full max-w-[44rem]'):
+            async def copy_code():
+                await ui.run_javascript('navigator.clipboard.writeText(`' + code + '`)', respond=False)
+                ui.notify('Copied to clipboard', type='positive', color='primary')
+            ui.markdown(f'````python\n{code}\n````')
+            ui.icon('content_copy', size='xs') \
+                .classes('absolute right-2 top-10 opacity-10 hover:opacity-80 cursor-pointer') \
+                .on('click', copy_code)
+        with browser_window(title=getattr(f, 'tab', None),
+                            classes='w-full max-w-[44rem] min-[1500px]:max-w-[20rem] min-h-[10rem] browser-window'):
+            intersection_observer(on_intersection=f)
+    return f
 
 
 def _window_header(bgcolor: str) -> ui.row():
@@ -67,16 +62,21 @@ def _title(title: str) -> None:
     ui.label(title).classes('text-sm text-gray-600 absolute left-1/2 top-[6px]').style('transform: translateX(-50%)')
 
 
-def _tab(name: str, color: str, bgcolor: str) -> None:
+def _tab(content: Union[str, Callable], color: str, bgcolor: str) -> None:
     with ui.row().classes('gap-0'):
         with ui.label().classes(f'w-2 h-[24px] bg-[{color}]'):
             ui.label().classes(f'w-full h-full bg-[{bgcolor}] rounded-br-[6px]')
-        ui.label(name).classes(f'text-sm text-gray-600 px-6 py-1 h-[24px] rounded-t-[6px] bg-[{color}]')
+        with ui.row().classes(f'text-sm text-gray-600 px-6 py-1 h-[24px] rounded-t-[6px] bg-[{color}] items-center gap-2'):
+            if callable(content):
+                content()
+            else:
+                ui.label(content)
         with ui.label().classes(f'w-2 h-[24px] bg-[{color}]'):
             ui.label().classes(f'w-full h-full bg-[{bgcolor}] rounded-bl-[6px]')
 
 
-def window(color: str, bgcolor: str, *, title: str = '', tab: str = '', classes: str = '') -> ui.column:
+def window(color: str, bgcolor: str, *,
+           title: str = '', tab: Union[str, Callable] = '', classes: str = '') -> ui.column:
     with ui.card().classes(f'no-wrap bg-[{color}] rounded-xl p-0 gap-0 {classes}') \
             .style('box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1)'):
         with _window_header(bgcolor):
@@ -96,5 +96,5 @@ def bash_window(*, classes: str = '') -> ui.card:
     return window(BASH_COLOR, BASH_BGCOLOR, title='bash', classes=classes).classes('p-2 bash-window')
 
 
-def browser_window(title: Optional[str] = None, *, classes: str = '') -> ui.card:
+def browser_window(title: Optional[Union[str, Callable]] = None, *, classes: str = '') -> ui.card:
     return window(BROWSER_COLOR, BROWSER_BGCOLOR, tab=title or 'NiceGUI', classes=classes).classes('p-4 browser-window')

+ 2 - 6
website/documentation.py

@@ -633,11 +633,7 @@ def create_full() -> None:
 
     heading('Configuration')
 
-    @element_demo(ui.run, browser_title='My App')
-    def ui_run_demo():
-        ui.label('page with custom title')
-
-        # ui.run(title='My App')
+    load_demo(ui.run)
 
     # HACK: switch color to white for the next demo
     demo_BROWSER_BGCOLOR = demo.BROWSER_BGCOLOR
@@ -650,7 +646,7 @@ def create_full() -> None:
         Pick any parameter as it is defined by the internally used [pywebview module](https://pywebview.flowrl.com/guide/api.html) 
         for the `webview.create_window` and `webview.start` functions.
         Note that these keyword arguments will take precedence over the parameters defined in ui.run.
-    ''')
+    ''', tab=lambda: ui.label('NiceGUI'))
     def native_mode_demo():
         from nicegui import app
 

+ 6 - 5
website/documentation_tools.py

@@ -65,15 +65,17 @@ def render_docstring(doc: str, with_params: bool = True) -> ui.html:
 
 class text_demo:
 
-    def __init__(self, title: str, explanation: str) -> None:
+    def __init__(self, title: str, explanation: str, tab: Optional[Union[str, Callable]] = None) -> None:
         self.title = title
         self.explanation = explanation
         self.make_menu_entry = True
+        self.tab = tab
 
     def __call__(self, f: Callable) -> Callable:
         subheading(self.title, make_menu_entry=self.make_menu_entry)
         ui.markdown(self.explanation).classes('bold-links arrow-links')
-        return demo()(f)
+        f.tab = self.tab
+        return demo(f)
 
 
 class intro_demo(text_demo):
@@ -85,9 +87,8 @@ class intro_demo(text_demo):
 
 class element_demo:
 
-    def __init__(self, element_class: Union[Callable, type], browser_title: Optional[str] = None) -> None:
+    def __init__(self, element_class: Union[Callable, type]) -> None:
         self.element_class = element_class
-        self.browser_title = browser_title
 
     def __call__(self, f: Callable, *, more_link: Optional[str] = None) -> Callable:
         doc = self.element_class.__doc__ or self.element_class.__init__.__doc__
@@ -95,7 +96,7 @@ class element_demo:
         with ui.column().classes('w-full mb-8 gap-2'):
             subheading(title, more_link=more_link)
             render_docstring(documentation, with_params=more_link is None)
-            return demo(browser_title=self.browser_title)(f)
+            return demo(f)
 
 
 def load_demo(api: Union[type, Callable]) -> None:

+ 58 - 0
website/more_documentation/run_documentation.py

@@ -1,7 +1,65 @@
 from nicegui import ui
 
+from ..documentation_tools import text_demo
+
 
 def main_demo() -> None:
     ui.label('page with custom title')
 
     # ui.run(title='My App')
+main_demo.tab = 'My App'
+
+
+def more() -> None:
+    @text_demo('Emoji favicon', '''
+        You can use an emoji as favicon.
+        This works in Chrome, Firefox and Safari.
+    ''', tab=lambda: ui.markdown('🚀&nbsp; NiceGUI'))
+    def emoji_favicon():
+        ui.label('NiceGUI Rocks!')
+
+        # ui.run(favicon='🚀')
+
+    @text_demo(
+        'Base64 favicon', '''
+        You can also use an base64-encoded image as favicon.
+    ''', tab=lambda: (
+            ui.image('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==')
+            .classes('w-4 h-4'),
+            ui.label('NiceGUI'),
+        ),
+    )
+    def base64_favicon():
+        ui.label('NiceGUI with a red dot!')
+
+        icon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='
+
+        # ui.run(favicon=icon)
+
+    @text_demo('SVG favicon', '''
+        And directly use an SVG as favicon.
+        Works in Chrome, Firefox and Safari.
+    ''', tab=lambda: (
+        ui.html('''
+            <svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
+                <circle cx="100" cy="100" r="78" fill="#ffde34" stroke="black" stroke-width="3" />
+                <circle cx="80" cy="85" r="8" />
+                <circle cx="120" cy="85" r="8" />
+                <path d="m60,120 C75,150 125,150 140,120" style="fill:none; stroke:black; stroke-width:8; stroke-linecap:round" />
+            </svg>
+        ''').classes('w-4 h-4'),
+        ui.label('NiceGUI'),
+    ))
+    def svg_favicon():
+        ui.label('NiceGUI makes you smile!')
+
+        smiley = '''
+            <svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
+                <circle cx="100" cy="100" r="78" fill="#ffde34" stroke="black" stroke-width="3" />
+                <circle cx="80" cy="85" r="8" />
+                <circle cx="120" cy="85" r="8" />
+                <path d="m60,120 C75,150 125,150 140,120" style="fill:none; stroke:black; stroke-width:8; stroke-linecap:round" />
+            </svg>
+        '''
+
+        # ui.run(favicon=smiley)