mirror of
https://expo.survex.com/repositories/expoweb/.git/
synced 2025-12-08 14:54:28 +00:00
now link to Google Keep exported to-do items
This commit is contained in:
254
handbook/troggle/keep_export.html
Normal file
254
handbook/troggle/keep_export.html
Normal file
File diff suppressed because one or more lines are too long
302
handbook/troggle/process_keep.py
Normal file
302
handbook/troggle/process_keep.py
Normal file
@@ -0,0 +1,302 @@
|
||||
import os
|
||||
import json
|
||||
|
||||
""" This was written almost entirely by Google Gemini 2.5 (apps and code) on 18 Sept. 2025
|
||||
|
||||
Google Takeout is accessible from the browser when logged in to your Google account.
|
||||
It can be set up to export dozens of different types of data. Here we only export "Keep"
|
||||
data.
|
||||
After running this, move the generated file to expoweb/handbook/troggle
|
||||
and git add/commit/push
|
||||
Philip Sargent
|
||||
"""
|
||||
# --- CONFIGURATION ---
|
||||
# 📌 IMPORTANT: Replace this with the actual path to your Google Keep Takeout folder.
|
||||
TAKEOUT_DIRECTORY = 'Takeout/Keep' # Example for Linux/macOS
|
||||
#
|
||||
|
||||
def process_keep_files_to_dict(directory_path):
|
||||
"""
|
||||
Scans a directory for Google Keep JSON files, filters for a specific
|
||||
label, prints the content, and returns the data as a list of dictionaries.
|
||||
|
||||
Args:
|
||||
directory_path (str): The path to the folder containing the JSON files.
|
||||
|
||||
Returns:
|
||||
list: A list of dictionaries, where each dictionary represents a note.
|
||||
"""
|
||||
print(f"📁 Processing files in: {directory_path}\n")
|
||||
|
||||
# This list will store the dictionary for each matching note
|
||||
extracted_notes = []
|
||||
|
||||
if not os.path.isdir(directory_path):
|
||||
print(f"❌ Error: Directory not found at '{directory_path}'.")
|
||||
print("Please update the TAKEOUT_DIRECTORY variable with the correct path.")
|
||||
return extracted_notes
|
||||
|
||||
# Iterate over every file in the specified directory
|
||||
for filename in os.listdir(directory_path):
|
||||
if filename.endswith('.json'):
|
||||
file_path = os.path.join(directory_path, filename)
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
# --- Label Check ---
|
||||
has_expo_label = False
|
||||
if 'labels' in data:
|
||||
if any(label.get('name', '').lower() == 'expo' for label in data['labels']):
|
||||
has_expo_label = True
|
||||
|
||||
if not has_expo_label:
|
||||
continue # Skip this file
|
||||
|
||||
# --- Data Extraction & Storage ---
|
||||
|
||||
# Create a dictionary to hold the current note's data
|
||||
current_note = {
|
||||
'title': data.get('title') or 'Untitled Note',
|
||||
'source_file': filename,
|
||||
'content_type': None,
|
||||
'color': data.get('color', 'DEFAULT'),
|
||||
'content': None
|
||||
}
|
||||
|
||||
# As before, print to the console for immediate feedback
|
||||
print("-" * 40)
|
||||
print(f"✅ Title: {current_note['title']}")
|
||||
|
||||
# Handle text notes
|
||||
if 'textContent' in data:
|
||||
content = data['textContent']
|
||||
current_note['content_type'] = 'text'
|
||||
current_note['content'] = content
|
||||
print(content)
|
||||
|
||||
# Handle list notes
|
||||
elif 'listContent' in data:
|
||||
current_note['content_type'] = 'list'
|
||||
list_items = []
|
||||
checked_items = []
|
||||
# print("Tasks:")
|
||||
for item in data['listContent']:
|
||||
# Create a clean dictionary for the list item
|
||||
item_data = {
|
||||
'text': item.get('text', ''),
|
||||
'is_checked': item.get('isChecked', False)
|
||||
}
|
||||
list_items.append(item_data)
|
||||
if item_data['is_checked']:
|
||||
checked_items.append(item_data)
|
||||
|
||||
# Print the list item to the console
|
||||
status = 'x' if item_data['is_checked'] else ' '
|
||||
# print(f" [{status}] {item_data['text']}")
|
||||
print(f"Tasks: {len(list_items)} of which {len(checked_items)} are done.")
|
||||
current_note['content'] = list_items
|
||||
|
||||
# Add the completed dictionary for this note to our main list
|
||||
extracted_notes.append(current_note)
|
||||
|
||||
# print("-" * 40 + "\n")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❗️ An unexpected error occurred with file {filename}: {e}")
|
||||
|
||||
if not extracted_notes:
|
||||
print("No notes with the 'EXPO' label were found in the directory.")
|
||||
|
||||
return extracted_notes
|
||||
|
||||
import html
|
||||
from datetime import datetime
|
||||
|
||||
def write_html_output(notes_data, filename="keep_export.html"):
|
||||
"""
|
||||
Generates an HTML file from the extracted Keep notes data.
|
||||
|
||||
Args:
|
||||
notes_data (list): A list of note dictionaries.
|
||||
filename (str): The name of the output HTML file.
|
||||
"""
|
||||
|
||||
# These are the standard Google Keep colors.
|
||||
color_map = {
|
||||
"DEFAULT": "#ffffff",
|
||||
"RED": "#f28b82",
|
||||
"ORANGE": "#fbbc04",
|
||||
"YELLOW": "#fff475",
|
||||
"GREEN": "#ccff90",
|
||||
"TEAL": "#a7ffeb",
|
||||
"BLUE": "#cbf0f8",
|
||||
"DARK_BLUE": "#aecbfa",
|
||||
"PURPLE": "#d7aefb",
|
||||
"PINK": "#fdcfe8",
|
||||
"BROWN": "#e6c9a8",
|
||||
"GRAY": "#e8eaed",
|
||||
}
|
||||
|
||||
# --- HTML Head and CSS Styling ---
|
||||
# The CSS is embedded directly in the HTML file for simplicity.
|
||||
html_head = f"""
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Google Keep Export</title>
|
||||
<style>
|
||||
body {{
|
||||
font-family: 'Google Sans', Roboto, Arial, sans-serif;
|
||||
background-color: #f1f3f4;
|
||||
margin: 0;
|
||||
padding: 24px;
|
||||
}}
|
||||
.container {{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 16px;
|
||||
}}
|
||||
.note-card {{
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.12);
|
||||
word-wrap: break-word; /* Prevents long text from overflowing */
|
||||
}}
|
||||
.note-title {{
|
||||
font-size: 1.1em;
|
||||
font-weight: 500;
|
||||
margin: 0 0 12px 0;
|
||||
}}
|
||||
.note-content p {{
|
||||
margin: 0;
|
||||
white-space: pre-wrap; /* Respects newlines in text notes */
|
||||
}}
|
||||
.task-list {{
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}}
|
||||
.task-item {{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}}
|
||||
.task-item input[type="checkbox"] {{
|
||||
margin-right: 10px;
|
||||
}}
|
||||
.completed-item-text {{
|
||||
text-decoration: line-through;
|
||||
color: #5f6368;
|
||||
}}
|
||||
.completed-items {{
|
||||
margin-top: 16px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
padding-top: 12px;
|
||||
}}
|
||||
.completed-items summary {{
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
color: #3c4043;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
"""
|
||||
|
||||
# --- HTML Body Generation ---
|
||||
html_body_content = ""
|
||||
for note in notes_data:
|
||||
# Sanitize title to prevent HTML injection issues
|
||||
title = html.escape(note['title'])
|
||||
|
||||
# Start building the content for this specific note
|
||||
note_content_html = ""
|
||||
|
||||
# MODIFICATION 2: Get the color and apply it as an inline style
|
||||
note_color_name = note.get('color', 'DEFAULT')
|
||||
# Safely get the color from the map, falling back to the default white
|
||||
bg_color = color_map.get(note_color_name, color_map['DEFAULT'])
|
||||
|
||||
# Handle text notes
|
||||
if note['content_type'] == 'text':
|
||||
# Sanitize text content and replace newlines with <br> tags
|
||||
text = html.escape(note['content'])
|
||||
note_content_html = f'<div class="note-content"><p>{text}</p></div>'
|
||||
|
||||
# Handle list notes
|
||||
elif note['content_type'] == 'list':
|
||||
unchecked_items = []
|
||||
checked_items = []
|
||||
|
||||
for item in note['content']:
|
||||
# Sanitize list item text
|
||||
item_text = html.escape(item['text'])
|
||||
if item['is_checked']:
|
||||
checked_items.append(
|
||||
f'<li class="task-item"><input type="checkbox" disabled checked><span class="completed-item-text">{item_text}</span></li>'
|
||||
)
|
||||
else:
|
||||
unchecked_items.append(
|
||||
f'<li class="task-item"><input type="checkbox" disabled><span>{item_text}</span></li>'
|
||||
)
|
||||
|
||||
# Combine unchecked items
|
||||
note_content_html += '<ul class="task-list">' + "".join(unchecked_items) + '</ul>'
|
||||
|
||||
# Add checked items inside a collapsible <details> element
|
||||
if checked_items:
|
||||
item_count = len(checked_items)
|
||||
plural_s = 's' if item_count > 1 else ''
|
||||
note_content_html += f"""
|
||||
<details class="completed-items">
|
||||
<summary>{item_count} completed item{plural_s}</summary>
|
||||
<ul class="task-list">{"".join(checked_items)}</ul>
|
||||
</details>
|
||||
"""
|
||||
|
||||
# Combine title and content into a single note card
|
||||
html_body_content += f"""
|
||||
<div class="note-card" style="background-color: {bg_color};">
|
||||
<h2 class="note-title">{title}</h2>
|
||||
{note_content_html}
|
||||
</div>
|
||||
"""
|
||||
# --- Final HTML Assembly ---
|
||||
full_html = f"""
|
||||
{html_head}
|
||||
<body>
|
||||
<h1>Troggle coding and design to-do lists</h1>
|
||||
<p>(Exported from Philip Sargent's Google Keep notes on {datetime.now().strftime('%Y-%m-%d %H:%M')})
|
||||
<p>Contact Philip if you wish to share these lists on your Google Keep account.
|
||||
<div class="container">
|
||||
{html_body_content}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
# --- Writing to File ---
|
||||
try:
|
||||
with open(filename, 'w', encoding='utf-8') as f:
|
||||
f.write(full_html)
|
||||
print(f"\n🎉 Successfully wrote {len(notes_data)} notes to '{filename}'")
|
||||
except Exception as e:
|
||||
print(f"❗️ An error occurred while writing the file: {e}")
|
||||
|
||||
# --- Run the program ---
|
||||
if __name__ == "__main__":
|
||||
all_notes_data = process_keep_files_to_dict(TAKEOUT_DIRECTORY)
|
||||
|
||||
if all_notes_data:
|
||||
print("\n" + "="*50)
|
||||
print("🎉 Successfully processed all matching files.")
|
||||
print(f"Total notes extracted: {len(all_notes_data)}")
|
||||
# print("Data has been stored in the 'all_notes_data' variable.")
|
||||
|
||||
print("="*50)
|
||||
write_html_output(all_notes_data)
|
||||
Reference in New Issue
Block a user