import gradio as gr from fastapi import FastAPI, Request from datetime import datetime import json import pandas as pd import os from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler from functools import wraps shortcuts_list = [] # Add file change handler class JSONFileHandler(FileSystemEventHandler): def on_modified(self, event): if event.src_path.endswith('shortcuts.json'): load_shortcuts() # Add decorator for file operations def ensure_fresh_data(func): @wraps(func) def wrapper(*args, **kwargs): load_shortcuts() # Reload before each operation result = func(*args, **kwargs) return result return wrapper def save_shortcuts(): with open('shortcuts.json', 'w') as f: json.dump(shortcuts_list, f, default=str) load_shortcuts() def load_shortcuts(): global shortcuts_list if os.path.exists('shortcuts.json'): # Open the file in binary mode with no buffering to avoid cached data with open('shortcuts.json', 'rb', buffering=0) as f: data = f.read() shortcuts_list = json.loads(data.decode('utf-8')) for shortcut in shortcuts_list: shortcut['date_added'] = datetime.fromisoformat(shortcut['date_added']) else: shortcuts_list = [] @ensure_fresh_data def add_shortcut(name, tags, link, emojis, color_from, color_to, short_description): new_shortcut = { 'name': name.strip(), 'tags': [tag.strip() for tag in tags.split('/') if tag.strip()], 'link': link.strip(), 'emojis': emojis.strip(), 'color_from': color_from, 'color_to': color_to, 'short_description': short_description.strip(), 'pinned': False, 'favorited': False, 'date_added': datetime.now().isoformat() } shortcuts_list.append(new_shortcut) save_shortcuts() # Return updated HTML return update_display() @ensure_fresh_data def get_shortcuts_dataframe(sort_by='Recently Added', search_query='', filter_tags=[]): datafra = pd.DataFrame(shortcuts_list) if datafra.empty: return datafra # Apply search filter if search_query: datafra = datafra[datafra['name'].str.contains(search_query, case=False)] # Apply tag filters if filter_tags: datafra = datafra[datafra['tags'].apply(lambda tags: any(tag in tags for tag in filter_tags))] # Sort the DataFrame if sort_by == 'Alphabetical': datafra = datafra.sort_values('name') elif sort_by == 'Recently Added': datafra = datafra.sort_values('date_added', ascending=False) elif sort_by == 'Favorites': datafra = datafra.sort_values('favorited', ascending=False) # Reset index datafra = datafra.reset_index(drop=True) return datafra @ensure_fresh_data def generate_cards_html(datafra): if datafra.empty: return "

No shortcuts available.

" cards_html = '
' for idx, shortcut in datafra.iterrows(): # Determine pin icon based on state if shortcut['pinned']: pin_icon = "📌" pin_style = "opacity: 1;" else: pin_icon = "📍" pin_style = "opacity: 0.3; text-decoration: line-through;" # Determine favorite icon based on state if shortcut['favorited']: favorite_icon = "❤️" else: favorite_icon = "🤍" style = f""" background: linear-gradient(135deg, {shortcut['color_from']}, {shortcut['color_to']}); padding: 20px; border-radius: 10px; position: relative; color: white; width: 300px; margin: 10px; cursor: pointer; """ labels_html = "" if shortcut['pinned'] or shortcut['favorited']: labels_html = f"""
{pin_icon if shortcut['pinned'] else ''} {favorite_icon if shortcut['favorited'] else ''}
""" card_html = f"""
{labels_html}
{shortcut['emojis']}

{shortcut['name']}

{shortcut['short_description']}

""" cards_html += card_html cards_html += '
' return cards_html @ensure_fresh_data def update_display(sort_by='Recently Added', search_query='', filter_tags=[]): datafra = get_shortcuts_dataframe(sort_by, search_query, filter_tags) return generate_cards_html(datafra) @ensure_fresh_data def toggle_pin(index): index = int(index) if 0 <= index < len(shortcuts_list): shortcuts_list[index]['pinned'] = not shortcuts_list[index]['pinned'] save_shortcuts() # Return updated HTML return update_display() @ensure_fresh_data def toggle_favorite(index): index = int(index) if 0 <= index < len(shortcuts_list): shortcuts_list[index]['favorited'] = not shortcuts_list[index]['favorited'] save_shortcuts() # Return updated HTML return update_display() @ensure_fresh_data def delete_shortcut(index): index = int(index) if 0 <= index < len(shortcuts_list): del shortcuts_list[index] save_shortcuts() # Return updated HTML return update_display() load_shortcuts() # JavaScript code attached to window object js_code = f""" function my_func() {{ window.isCmdOrCtrl = false; window.addEventListener('keydown', function(e) {{ if (e.key === 'Meta' || e.key === 'Control') {{ window.isCmdOrCtrl = true; window.showDeleteIcons(); }} }}); window.addEventListener('keyup', function(e) {{ if (e.key === 'Meta' || e.key === 'Control') {{ window.isCmdOrCtrl = false; window.hideDeleteIcons(); }} }}); window.handleHover = function(event, idx) {{ if (window.isCmdOrCtrl) {{ document.getElementById('card-' + idx).style.display = 'block'; }} }}; window.handleHoverOut = function(event, idx) {{ document.getElementById('card-'+ idx).style.display = 'none'; }}; window.showDeleteIcons = function() {{ const deleteIcons = document.querySelectorAll('[id^="delete-"]'); deleteIcons.forEach(icon => {{ icon.style.display = 'block'; }}); }}; window.hideDeleteIcons = function() {{ const deleteIcons = document.querySelectorAll('[id^="delete-"]'); deleteIcons.forEach(icon => {{ icon.style.display = 'none'; }}); }}; window.togglePin = function(idx) {{ // Implement the delete functionality, e.g., call an API endpoint fetch('/toggle_pin', {{ method: 'POST', headers: {{ 'Content-Type': 'application/json' }}, body: JSON.stringify({{ index: idx }}) }}) .then(response => response.json()) .then(data => {{ // Update the grid display document.getElementById('grid_output').innerHTML = data.grid_html; }}); }}; window.deleteShortcut = function(idx) {{ // Implement the delete functionality, e.g., call an API endpoint fetch('/delete_shortcut', {{ method: 'POST', headers: {{ 'Content-Type': 'application/json' }}, body: JSON.stringify({{ index: idx }}) }}) .then(response => response.json()) .then(data => {{ // Update the grid display document.getElementById('grid_output').innerHTML = data.grid_html; }}); }}; }} """ # Build the Gradio App with gr.Blocks(theme="charbelgrower/Crystal", js=js_code) as demo: gr.Markdown("## Website Shortcuts") with gr.Row(): search_bar = gr.Textbox(label="Search") sort_options = gr.Dropdown(choices=['Recently Added', 'Alphabetical', 'Favorites'], label="Sort By", value='Recently Added') # Collect all unique tags def get_all_tags(): return list(set(tag for shortcut in shortcuts_list for tag in shortcut['tags'])) filter_tags = gr.CheckboxGroup(choices=get_all_tags(), label="Filter Tags") grid_output = gr.HTML(value=update_display(), elem_id="grid_output") gr.Markdown("## Add a New Website Shortcut") with gr.Row(): name = gr.Textbox(label="Name") link = gr.Textbox(label="Link") with gr.Row(): tags = gr.Textbox(label="Tags (separate levels with '/')") emojis = gr.Textbox(label="Emojis") color_from = gr.ColorPicker(label="Gradient Color From") color_to = gr.ColorPicker(label="Gradient Color To") short_description = gr.Textbox(label="Short Description") add_button = gr.Button("Add Shortcut") # Update display when filters change def refresh_display(search_query='', sort_by='Recently Added', filter_tags=[]): grid_html = update_display(sort_by, search_query, filter_tags) filter_tags_options = get_all_tags() return grid_html, gr.update(choices=filter_tags_options) search_bar.change(fn=refresh_display, inputs=[search_bar, sort_options, filter_tags], outputs=grid_output) sort_options.change(fn=refresh_display, inputs=[search_bar, sort_options, filter_tags], outputs=grid_output) filter_tags.change(fn=refresh_display, inputs=[search_bar, sort_options, filter_tags], outputs=grid_output) # Add shortcut action add_button.click( fn=add_shortcut, inputs=[name, tags, link, emojis, color_from, color_to, short_description], outputs=grid_output ) # Expose endpoints for custom functions using FastAPI api = FastAPI() @api.post('/delete_shortcut') async def delete_shortcut_endpoint(request: Request): data = await request.json() index = data.get('index') grid_html = delete_shortcut(index) return {'grid_html': grid_html} @api.post('/toggle_pin') async def toggle_pin_endpoint(request: Request): data = await request.json() index = data.get('index') grid_html = toggle_pin(index) return {'grid_html': grid_html} @api.post('/toggle_favorite') async def toggle_favorite_endpoint(request: Request): data = await request.json() index = data.get('index') grid_html = toggle_favorite(index) return {'grid_html': grid_html} app = gr.mount_gradio_app(api, demo,"/") demo.launch(ssr_mode=False) # Initialize file watcher if __name__ == "__main__": import uvicorn demo.unload(fn=load_shortcuts) event_handler = JSONFileHandler() observer = Observer() observer.schedule(event_handler, path='shortcuts.json', recursive=False) observer.start() try: uvicorn.run(app, host="0.0.0.0", port=7640) finally: observer.stop() observer.join()