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()