This application provides a simple web-based interface for managing tasks stored in the todo.txt format. It allows users to add, edit, complete, delete, filter, and import tasks. It leverages localStorage
for local persistence, a service worker for offline capabilities, and now integrates with Dropbox to synchronize the todo.txt
file across devices.
A live version of the app is available here, and the full code on GitHub here.
index.html
)The index.html
file sets up the user interface using Bootstrap 5 for layout and components. It includes elements for task input, filtering, actions, the task list itself, and UI feedback for Dropbox synchronization.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="app-version" content="2.0.3">
<title>Html5 Application - Todo.txt Webapp</title>
<!-- CSS Includes (Bootstrap, Datepicker, Custom) -->
<link rel="stylesheet" href="/assets/css/lib/personal-site-theme.min.css?id=af8c74">
<link rel="stylesheet" href="/assets/css/ui/datepicker.min.css?id=a4db0c">
<link rel="stylesheet" href="/assets/css/apps/sandbox/todotxt/todo.min.css?id=b5580b">
<!-- ... other meta tags, icons, manifest ... -->
</head>
<body id="page-top">
<div class="container">
<!-- Header with Title and Dropbox Sync Status/Button -->
<div class="d-flex justify-content-between align-items-center mb-3">
<h2 class="mb-0">Todo.txt</h2>
<div class="d-flex align-items-center">
<span id="syncStatusIndicator" class="me-2 text-muted small"></span>
<button type="button" id="dropboxAuthButton" class="btn btn-light p-1">
<i class="fa-brands fa-dropbox"></i>
</button>
</div>
</div>
<!-- Add Todo Section -->
<div class="add-todo-section mb-3">
<!-- Dropdown Buttons for Priority, Project, Context -->
<div class="input-group mb-2">
<div class="col-md-4 col-12">
<div class="dropdown">
<button class="btn btn-primary dropdown-toggle w-100" id="priorityDropdownButton">Priority</button>
<input type="hidden" id="prioritySelect">
<ul class="dropdown-menu" id="priorityDropdownMenu"> <!-- Options populated by JS --> </ul>
</div>
</div>
<!-- Project & Context Dropdowns similar structure -->
<!-- ... -->
</div>
<!-- Date Pickers -->
<div class="mb-2 row">
<div class="col-12 col-md-6">
<label for="createdDate">Created:</label>
<input type="text" class="form-control date-picker" id="createdDate">
</div>
<div class="col-12 col-md-6">
<label for="dueDate">Due:</label>
<input type="text" class="form-control date-picker" id="dueDate">
</div>
</div>
<!-- Main Input and Add/Save Button -->
<div class="row g-2 mb-2 align-items-md-stretch">
<div class="col-12 col-md">
<input type="text" class="form-control" placeholder="Enter todo item" id="todoInput">
</div>
<div class="col-12 col-md-auto">
<button class="btn btn-primary" type="button" id="addButton">Add Todo</button>
</div>
</div>
<!-- Action Buttons (Filter, Copy, Import) -->
<div class="row mt-2 gx-0 gx-md-2">
<div class="col"><button class="btn btn-primary w-100" id="filterButton">Filter</button></div>
<div class="col"><button class="btn btn-primary w-100" id="copyAllButton">Copy</button></div>
<div class="col"><button class="btn btn-primary w-100" id="importButton">Import</button></div>
</div>
<textarea class="form-control mt-2" id="importTextarea" style="display:none;"></textarea>
</div>
<!-- End Add Todo Section -->
<!-- Todo List Container -->
<ul class="list-group jsTodoTxt todo-list" id="todo-list"></ul>
</div>
<!-- Conflict Resolution Modal -->
<div class="modal fade" id="conflictModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="conflictModalLabel">Sync Conflict Detected</h5>
<!-- ... close button ... -->
</div>
<div class="modal-body">
<p>Which version would you like to keep?</p>
<p><strong>Local:</strong> <span id="localConflictTime"></span></p>
<p><strong>Dropbox:</strong> <span id="dropboxConflictTime"></span></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" id="keepLocalButton">Keep Local</button>
<button type="button" class="btn btn-complementary" id="keepDropboxButton">Keep Dropbox</button>
</div>
</div>
</div>
</div>
<!-- JavaScript Includes -->
<script src="/assets/js/lib/jquery-3.7.1.slim.min.js"></script>
<script src="/assets/js/lib/bootstrap-5.3.2.min.js"></script>
<script src="/assets/js/lib/bootstrap-datepicker-1.10.0.min.js"></script>
<script src="/assets/js/lib/dropbox-sdk-10.34.0.min.js"></script>
<script src="/assets/js/lib/jstodotxt.min.js?id=ea443f"></script>
<!-- ... other libs ... -->
<!-- Application Modules (Type Module) -->
<script src="/assets/js/apps/sandbox/todotxt/dropbox-sync.js?id=f2fe62" type="module"></script>
<script src="/assets/js/apps/sandbox/todotxt/todo.js?id=b444cb" type="module"></script>
<script src="/assets/js/apps/sandbox/todotxt/todo-datepicker.min.js?id=ca1e09"></script>
<!-- ... other modules ... -->
<script>
// Service Worker Registration
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/apps/sandbox/todotxt/service-worker.min.js?id=ae9e28', { /* ... */ })
.catch(err => console.error('SW registration failed:', err));
}
</script>
</body>
</html>
Key elements:
#syncStatusIndicator
), Dropbox button (#dropboxAuthButton
).#priorityDropdownButton
, etc. with hidden inputs #prioritySelect
, etc.), Date pickers (#createdDate
, #dueDate
), main input (#todoInput
), Add/Save button (#addButton
), Action buttons (#filterButton
, #copyAllButton
, #importButton
), hidden import textarea (#importTextarea
).#todo-list
).#conflictModal
).dropbox-sync.js
, todo.js
, todo-datepicker.js
, etc.).assets/css/apps/sandbox/todotxt/todo.min.css
)The custom CSS file provides specific styling, including theme adjustments, responsive layouts, and styles for the new UI components.
/* Base container margin */
.container { margin-top: 20px; }
/* List item layout and base style */
.list-group-item {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #2C2C2C; /* Dark background */
/* ... padding, border ... */
}
/* Custom dropdown menu appearance */
.dropdown-menu { /* ... dark theme variables ... */ }
/* Input/Button Rounded Corners (responsive) */
.btn-rounded { /* ... */ }
.form-control-todo { /* ... */ }
.form-control-date-left { /* ... */ }
/* ... other responsive rounding styles ... */
/* Datepicker Positioning (responsive) */
.datepicker-left { /* ... positioning for createdDate picker ... */ }
.datepicker-right { /* ... positioning for dueDate picker ... */ }
/* ... responsive adjustments ... */
/* Dropbox Button Styling */
.dropbox-auth-button { /* ... specific styles ... */ }
.dropbox-auth-button i { /* ... icon style ... */ }
Key styling points:
The application logic is modularized into several JavaScript files within assets/js/apps/sandbox/todotxt/
, including dedicated modules for Dropbox integration.
todo.js
)Acts as the central coordinator, initializes subsystems, and defines core task actions.
// Imports: Loaders, Storage, UI, Handlers, Dropdowns, *Dropbox Sync*
import { loadTodos } from './todo-load.js';
import { setupDropdownHandlers } from './todo-dropdowns.js';
import { initializeDropboxSync } from './dropbox-sync.js'; // <-- Import Dropbox initializer
import './todo-event-handlers.js'; // Attaches general handlers
import './todo-import.js'; // Attaches import handlers
// ... other imports ...
// Helper: Format date for datepicker (YYYY-MM-DD -> MM/DD/YYYY)
function formatDateForPicker(dateInput) { /* ... */ }
// Selectors for UI elements (jQuery)
const todoInput = $('#todoInput');
const addButton = $('#addButton');
// ... other selectors (todoList, prioritySelect, etc.) ...
// Export elements/functions needed by other modules
export { toggleTodoCompletion, startEditTodo, deleteTodoItem, /* ... */ };
// Toggle task completion status
function toggleTodoCompletion(listItem) {
// ... (get item ID and text, parse with jsTodoTxt) ...
item.setComplete(!item.complete());
if (item.complete()) {
// ... (clear priority, set completion date if created date exists) ...
}
updateTodoInStorage(itemId, item); // Update storage (triggers Dropbox upload)
// ... (update UI styles, text, button title) ...
loadTodos(todoList); // Reload list to re-sort
}
// Initiate editing a task
function startEditTodo(listItem) {
// ... (get item ID and text, parse with jsTodoTxt) ...
todoInput.val(itemText); // Populate input
addButton.text('Save Edit').data('editingId', itemId);
// ... (populate hidden inputs, update dropdown button text) ...
// ... (populate date pickers using formatDateForPicker) ...
listItem.remove(); // Remove from UI temporarily
}
// Delete a task
function deleteTodoItem(listItem) {
const itemId = listItem.data('id');
removeTodoFromStorage(itemId); // Remove from storage (triggers Dropbox upload)
listItem.remove(); // Remove from UI
}
// Initialize on document ready
$(document).ready(function () {
setupDropdownHandlers(); // Set up new dropdowns
loadTodos(todoList); // Load initial tasks
initializeDropboxSync(); // Initialize Dropbox sync system
});
initializeDropboxSync
. Sets up dropdowns, loads initial todos, and starts the Dropbox sync process on page load.toggleTodoCompletion
, startEditTodo
, and deleteTodoItem
, updated to handle new UI elements (date pickers, dropdowns) and task properties (completion date).todo-storage.js
)Handles localStorage
interactions and now triggers Dropbox uploads.
import { uploadTodosToDropbox } from './dropbox-sync.js'; // Import the upload function
const LOCAL_STORAGE_KEY = 'todos';
const LOCAL_TIMESTAMP_KEY = 'todosLastModifiedLocal'; // For conflict detection
// Generate unique IDs
export function generateUniqueId() { /* ... */ }
// Get todos from localStorage (handles migration)
export function getTodosFromStorage() { /* ... returns array of {id, text} ... */ }
// Save array of {id, text} objects to localStorage
export function saveTodosToStorage(todoObjects) {
// ... (validation) ...
localStorage.setItem(LOCAL_STORAGE_KEY, /* ... */);
localStorage.setItem(LOCAL_TIMESTAMP_KEY, new Date().toISOString()); // Update timestamp
}
// Get the timestamp of the last local save
export function getLocalLastModified() { /* ... returns timestamp string ... */ }
// Add a new todo string, save, then trigger upload
export function addTodoToStorage(itemText) {
// ... (get existing todos, create new object) ...
saveTodosToStorage(todos);
uploadTodosToDropbox().catch(err => console.error("Upload after add failed:", err)); // Trigger upload
}
// Update the text of an existing todo, save, then trigger upload
export function updateTodoInStorage(idToUpdate, newItem) {
// ... (find index, update text) ...
saveTodosToStorage(todos);
uploadTodosToDropbox().catch(err => console.error("Upload after update failed:", err)); // Trigger upload
}
// Remove a todo by ID, save, then trigger upload
export function removeTodoFromStorage(idToDelete) {
// ... (filter out item) ...
saveTodosToStorage(todos);
uploadTodosToDropbox().catch(err => console.error("Upload after delete failed:", err)); // Trigger upload
// ...
}
localStorage
key todosLastModifiedLocal
.uploadTodosToDropbox()
after any successful local modification (add
, update
, remove
) to keep Dropbox synchronized.todo-load.js
, todo-ui.js
, todo-list-display.js
)These modules handle fetching data, sorting, rendering, and UI helpers.
// --- todo-load.js ---
import { getTodosFromStorage } from './todo-storage.js';
import { addTodoToList as renderTodoItem } from './todo-list-display.js';
import { updateDropdowns } from './todo-dropdowns.js';
// ... imports ...
export function loadTodos(todoList) {
const todoObjects = getTodosFromStorage(); // Get {id, text}
// ... (map to objects with parsed items using jsTodoTxt.Item) ...
// ... (sort items: incomplete first, then by priority) ...
todoList.empty(); // Clear current list
// Render sorted items using renderTodoItem from todo-list-display.js
itemsForSorting.forEach(sortedItem => {
renderTodoItem(sortedItem, sortedItem.item, todoList);
});
updateDropdowns(itemsForSorting.map(i => i.item)); // Update dropdown options
}
// --- todo-ui.js ---
// Helper: Get priority color
function getPriorityColor(priority) { /* ... returns color hex ... */ }
// Helper: Apply styles (completion, priority color)
export function applyItemStyles(listItem, item) { /* ... adds/removes classes, sets styles ... */ }
// Helper: Create main text span with click-to-copy
export function createTodoSpan(item) { /* ... returns jQuery span object ... */ }
// --- todo-list-display.js ---
import { applyItemStyles, createTodoSpan } from './todo-ui.js';
import { toggleTodoCompletion, startEditTodo, deleteTodoItem } from './todo.js'; // Actions
// Renders a single todo item to the list
export function addTodoToList(sortedItemData, item, todoList) {
const listItem = $('<li></li>').addClass('list-group-item').data('id', sortedItemData.id);
const todoSpan = createTodoSpan(item); // Use UI helper
// Create button group (Check, Edit, Delete) inline
const buttonGroup = $('<div>').css({'display': 'flex', /* ... */});
// ... (create checkButton, attach toggleTodoCompletion) ...
// ... (create editButton, attach startEditTodo) ...
// ... (create deleteButton, attach deleteTodoItem) ...
listItem.append(todoSpan, buttonGroup);
applyItemStyles(listItem, item); // Use UI helper
todoList.append(listItem);
}
todo-load.js
): Fetches from storage, parses, sorts, clears the UI, renders items using todo-list-display.js
, and updates dropdowns.todo-ui.js
): Provides utilities for styling and element creation.todo-list-display.js
): Creates the DOM structure for each task, including text and action buttons with attached event handlers calling functions from todo.js
.todo-event-handlers.js
)Attaches listeners for the main controls, incorporating date pickers and new dropdown logic.
// Imports: Storage, Loaders, UI elements, Actions
import { /* ... UI elements ... */ } from './todo.js';
import { addTodoToStorage, removeTodoFromStorage, getTodosFromStorage } from './todo-storage.js';
import { loadTodos } from './todo-load.js';
// ... other imports ...
// Helper: Format date for todo.txt string (MM/DD/YYYY -> YYYY-MM-DD)
function formatDateForTodoTxt(dateString) { /* ... */ }
$(document).ready(function () {
// Add/Save Button Click Handler
addButton.click(function () {
const editingId = addButton.data('editingId');
if (editingId) {
// Handle Saving Edit
// ... (get input text) ...
removeTodoFromStorage(editingId); // Remove old (triggers upload)
const item = new jsTodoTxt.Item(newTextFromInput);
// ... (get priority, project, context, dates from UI) ...
// ... (apply updates to 'item' object using jsTodoTxt methods) ...
addTodoToStorage(item.toString()); // Add updated item (triggers upload)
// ... (reset UI elements: input, dropdowns, datepickers) ...
loadTodos(todoList); // Reload
} else {
// Handle Adding New Todo
// ... (get input text, priority, project, context, dates from UI) ...
// ... (construct todoText string) ...
addTodoToStorage(todoText.trim()); // Add new item (triggers upload)
// ... (reset UI elements) ...
loadTodos(todoList); // Reload
}
});
// Copy All Button (Clipboard.js)
const clipboard = new ClipboardJS('#copyAllButton', {
text: function() { /* ... get todos, sort, return as string ... */ }
});
// ... success/error handlers ...
// Filter Button Click Handler
filterButton.click(function() {
// ... (get filter criteria from hidden inputs: priority, project, context) ...
// ... (get todos from storage, create jsTodoTxt.List) ...
// ... (build filterCriteria object) ...
if (Object.keys(filterCriteria).length === 0) { loadTodos(todoList); return; }
const filteredItems = list.filter(filterCriteria); // Filter
todoList.empty(); // Clear UI list
// ... (sort filtered items) ...
// ... (find original {id, text} for each filtered item) ...
// ... (render filtered items using function from todo-list-display.js) ...
});
});
todo-import.js
)Manages importing tasks from the textarea.
// Imports: Storage, Loaders, UI elements
import { addTodoToStorage } from './todo-storage.js';
import { loadTodos } from './todo-load.js';
// ... other imports ...
$(document).ready(function () {
// ... (get importButton, importTextarea) ...
importButton.on('click', /* show textarea */);
importTextarea.on('blur', importTodosFromTextarea); // Import on blur
function importTodosFromTextarea() {
// ... (get text, split lines) ...
lines.forEach(line => {
// ... (trim line, validate using jsTodoTxt.Item) ...
addTodoToStorage(trimmedLine); // Adds to storage, triggers upload
// ...
});
if (itemsImported) loadTodos(todoList); // Reload list
// ... (clear/hide textarea) ...
}
});
addTodoToStorage
(which triggers uploads), and reloads the list.todo-dropdowns.js
)Handles the new Bootstrap dropdown components.
// Updates dropdown menus (<ul>) with options (<li><a>)
export function updateDropdowns(items) {
// ... (extract unique projects/contexts from items into Sets) ...
const projectDropdownMenu = document.getElementById('projectDropdownMenu');
const contextDropdownMenu = document.getElementById('contextDropdownMenu');
// ... (clear existing options in projectDropdownMenu, contextDropdownMenu) ...
// ... (add "Clear" option and separator to each menu) ...
// ... (iterate sorted projects/contexts, append <li><a> elements with data-value) ...
}
// Sets up click handlers for dropdown items using event delegation
export function setupDropdownHandlers() {
$(document).ready(function() {
// Delegate from static menu UL (e.g., #priorityDropdownMenu)
$('#priorityDropdownMenu').on('click', 'a.dropdown-item', function(e) {
e.preventDefault();
var value = $(this).data('value'); // Get value
$('#priorityDropdownButton').text($(this).text()); // Update button text
$('#prioritySelect').val(value); // Update hidden input
});
// ... (Similar delegation for project and context menus) ...
});
}
todo-datepicker.js
)Initializes the Bootstrap Datepicker component.
$(document).ready(function() {
// Initialize date pickers
$('.date-picker').each(function(){
$(this).datepicker({
templates: { /* ... FontAwesome icons ... */ },
orientation: "bottom left"
}).on('show', function() {
// ... (add 'open' class) ...
// ... (add 'datepicker-left' or 'datepicker-right' based on ID for CSS positioning) ...
// ... (apply optional color theme from data attribute) ...
}).on('hide', function() {
// ... (remove dynamic classes) ...
});
});
});
service-worker.js
, cache.js
)Implements offline functionality and cache management.
// --- service-worker.js ---
const CACHE_NAME = 'todotxt-cache-v1-0-0';
const assetsToCache = [
'/apps/sandbox/todotxt/index.html',
// ... All necessary CSS, JS (libs + app modules including Dropbox), JSON, images ...
];
// Install event: Cache assets
self.addEventListener('install', /* ... */);
// Activate event: Clean old caches, cache assets
self.addEventListener('activate', /* ... */);
// Fetch event: Serve index.html or assets from cache first, fallback to network
self.addEventListener('fetch', /* ... */);
// Message event: Handle 'refresh_cache' command
self.addEventListener('message', /* ... */);
// --- cache.js ---
document.addEventListener('DOMContentLoaded', function() {
const versionUrl = '/data/json/apps/sandbox/todotxt/version.min.json';
fetch(versionUrl)
.then(/* ... */)
.then(data => {
// ... (compare online vs local version) ...
if (onlineVersion !== localVersion) {
// ... (postMessage('refresh_cache') to SW) ...
}
})
.catch(/* ... */);
});
A major new feature is the integration with Dropbox, allowing users to connect their account and synchronize their todo.txt
file stored within the app’s dedicated Dropbox folder. This is handled by a set of modules within the js/dropbox/
directory.
dropbox-sync.js
)Entry point for the Dropbox system, orchestrating authentication and offline handling setup.
import { initializeAuthentication } from './dropbox/auth.js';
import { initializeOfflineHandling } from './dropbox/offline.js';
import { uploadTodosToDropbox } from './dropbox/api.js'; // Exported for use elsewhere
// Initializes the whole system
async function initializeDropboxSync() {
const authInitialized = await initializeAuthentication(); // Handles tokens/redirects
if (authInitialized) {
initializeOfflineHandling(); // Sets up online/offline listeners
} // ...
// Initial sync check is triggered within API initialization (called by auth)
}
export { initializeDropboxSync, uploadTodosToDropbox };
document.addEventListener('DOMContentLoaded', initializeDropboxSync); // Auto-init
uploadTodosToDropbox
for triggering uploads from the storage module.dropbox/config.js
)Stores constants: CLIENT_ID
, dynamic REDIRECT_URI
, TODO_FILENAME
, and localStorage
keys.
export const CLIENT_ID = 'YOUR_DROPBOX_APP_CLIENT_ID'; // Replace
export const REDIRECT_URI = /* ... dynamically calculated ... */;
export const TODO_FILENAME = '/todo.txt';
export const PENDING_UPLOAD_KEY = 'dropboxUploadPending';
export const ACCESS_TOKEN_KEY = 'dropboxAccessToken';
dropbox/auth.js
)Manages the Dropbox OAuth2 flow using Dropbox.DropboxAuth
.
import { CLIENT_ID, REDIRECT_URI, ACCESS_TOKEN_KEY } from './config.js';
import { updateAuthButton, /* ... */ } from './ui.js';
import { initializeDropboxApi } from './api.js'; // To init API after auth
// Initializes auth: checks token, handles redirect
export async function initializeAuthentication() {
// ... (init Dropbox.DropboxAuth) ...
accessToken = localStorage.getItem(ACCESS_TOKEN_KEY);
const redirected = await handleRedirect(); // Checks URL hash
if (accessToken && !redirected) {
await initializeDropboxApi(accessToken); // Init API if token exists
} // ...
updateAuthButton(!!accessToken, authenticateWithDropbox, logoutFromDropbox);
// ...
}
// Handles the redirect back from Dropbox OAuth
async function handleRedirect() {
// ... (parse token/error from window.location.hash) ...
if (token) {
// ... (store token, clean URL, init API, update UI) ...
await initializeDropboxApi(accessToken); // Init API with new token
return true;
} // ...
return false;
}
// Starts the OAuth flow by redirecting the user to Dropbox
export function authenticateWithDropbox() { /* ... redirects user ... */ }
// Logs out: clears token, API instance, updates UI
export function logoutFromDropbox() { /* ... clears storage/state, updates UI ... */ }
// Gets the current token
export function getAccessToken() { /* ... returns token ... */ }
initializeDropboxApi
) upon successful authentication.dropbox/api.js
)Core logic for Dropbox file operations and conflict resolution.
/* global Dropbox */
import { TODO_FILENAME } from './config.js';
import { updateSyncIndicator, showConflictModal, /* ... */ } from './ui.js';
import { setUploadPending, clearUploadPending } from './offline.js';
// Dynamic imports for todo-storage.js, todo-load.js used within functions
let dbx = null; // Main Dropbox API client instance
// Initialize the main Dropbox API client with token
export async function initializeDropboxApi(token) {
// ... (init Dropbox.Dropbox with token) ...
dbx = new Dropbox.Dropbox({ accessToken: token });
await syncWithDropbox(); // Trigger initial sync check
}
// Get file metadata (server_modified timestamp)
export async function getDropboxFileMetadata() { /* ... dbx.filesGetMetadata ... */ }
// Download the todo.txt file content
export async function downloadTodosFromDropbox() { /* ... dbx.filesDownload ... returns text ... */ }
// Upload local todos to Dropbox (handles conflicts before upload)
export async function uploadTodosToDropbox() {
if (!navigator.onLine) { setUploadPending(); return; } // Handle offline
// ... (check dbx initialized) ...
try {
// Dynamic imports for getLocalLastModified, getTodosFromStorage, etc.
const { getLocalLastModified } = await import('../todo-storage.js'); // etc.
// --- Pre-upload Conflict Check ---
const localDate = /* ... get local last save Date object ... */;
const dropboxDate = /* ... get Dropbox server_modified Date object ... */;
if (dropboxMeta && dropboxDate && localDate && dropboxDate > localDate) { // Conflict!
const userChoice = await showConflictModal(localDate, dropboxDate); // Show modal
if (userChoice === 'local') { /* Proceed to upload */ }
else if (userChoice === 'dropbox') { /* Download, overwrite local, return */ }
else { /* Cancel, return */ }
}
// --- End Conflict Check ---
if (proceedWithUpload) {
// ... (get current todos from storage) ...
// ... (format todos as newline-separated string) ...
await dbx.filesUpload({ path: TODO_FILENAME, contents: content, mode: 'overwrite', /*...*/ });
clearUploadPending();
}
} catch (error) { /* ... handle errors ... */ }
finally { /* ... update sync indicator ... */ }
}
// Core sync logic: compare local/remote, trigger actions
export async function syncWithDropbox() {
// ... (check dbx initialized and online) ...
try {
// Dynamic imports for storage/load functions
const localDate = /* ... get local last save Date ... */;
const dropboxDate = /* ... get Dropbox server_modified Date ... */;
// Case 1: No Dropbox file -> Upload local if exists
// Case 2: No local data -> Download Dropbox
// Case 3: Both exist - Compare timestamps (with buffer)
if (timeDiff <= buffer) { /* Synced */ }
else if (dropboxDate > localDate) { // Remote newer: CONFLICT -> showConflictModal
// ... (handle userChoice: upload or download) ...
} else { // Local newer: Upload
await uploadTodosToDropbox();
}
} catch (error) { /* ... handle sync errors ... */ }
finally { /* ... update sync indicator ... */ }
}
Dropbox.Dropbox
SDK client.todo.txt
.uploadTodosToDropbox
: Performs pre-upload conflict check using timestamps. If Dropbox is newer, prompts user via showConflictModal
. Uploads if no conflict or user chooses local. Called after local changes.syncWithDropbox
: Compares local/remote timestamps on initialization or potentially periodically. Handles cases where one side is missing, or triggers conflict resolution/upload based on which is newer.import()
to manage dependencies.dropbox/offline.js
)Manages behavior during network status changes.
import { PENDING_UPLOAD_KEY } from './config.js';
import { updateSyncIndicator, SyncStatus } from './ui.js';
import { getAccessToken } from './auth.js';
// Dynamic import for upload function
// Check/set/clear pending upload flag in localStorage
export function isUploadPending() { /* ... */ }
export function setUploadPending() { /* ... sets flag, updates UI indicator ... */ }
export function clearUploadPending() { /* ... */ }
// When app comes online
async function handleOnlineStatus() {
if (!getAccessToken()) return; // Ignore if not logged in
if (isUploadPending()) { // Check if changes occurred offline
// ... (show syncing indicator) ...
try { // Dynamically import and attempt upload
const { uploadTodosToDropbox } = await import('./api.js');
await uploadTodosToDropbox();
} catch (err) { /* ... handle error ... */ }
} // ...
}
// When app goes offline
function handleOfflineStatus() { /* ... update UI indicator ... */ }
// Initialize listeners and set initial status
export function initializeOfflineHandling() {
window.addEventListener('online', handleOnlineStatus);
window.addEventListener('offline', handleOfflineStatus);
// ... (set initial indicator based on navigator.onLine, pending flag, login status) ...
}
localStorage
(PENDING_UPLOAD_KEY
) to track if uploads were attempted while offline.online
/offline
events.dropbox/ui.js
)Manages UI elements related to Dropbox: the status indicator, conflict modal, and auth button.
import { logVerbose } from '../todo-logging.js';
export const SyncStatus = { IDLE: 'idle', SYNCING: 'syncing', /*...*/ };
// Updates the sync status indicator icon (#syncStatusIndicator)
export function updateSyncIndicator(status, message = '') {
// ... (switch statement sets icon class, text, title based on status) ...
// ... (updates indicator.innerHTML and indicator.title) ...
}
// --- Conflict Resolution Modal ---
let conflictModalInstance = null; // Bootstrap Modal instance
let conflictResolver = null; // Promise resolver
// Shows the Bootstrap conflict modal (#conflictModal)
export function showConflictModal(localDate, dropboxDate) {
// ... (init modal instance via new bootstrap.Modal if needed) ...
// ... (setup listeners *once* via setupConflictModalListeners) ...
// ... (populate #localConflictTime, #dropboxConflictTime) ...
conflictModalInstance.show();
return new Promise((resolve) => { conflictResolver = resolve; }); // Returns promise
}
// Sets up listeners for modal buttons/close events
function setupConflictModalListeners(modalElement) {
// ... (attach click handlers to #keepLocalButton, #keepDropboxButton) ...
// ... (handlers call conflictResolver with 'local', 'dropbox', or null) ...
// ... (handle modal close events) ...
}
// --- Auth Button ---
// Updates the Dropbox auth button (#dropboxAuthButton) icon, title, and click handler
export function updateAuthButton(isLoggedIn, loginHandler, logoutHandler) {
// ... (updates icon class, title attribute, and attaches login/logout handler) ...
}
// Initialize indicator on load
document.addEventListener('DOMContentLoaded', () => {
updateSyncIndicator(SyncStatus.NOT_CONNECTED);
});
updateSyncIndicator
to give visual feedback on sync state.showConflictModal
to handle user interaction during sync conflicts, returning a Promise.updateAuthButton
).todo-logging.js
)A simple utility for conditional verbose logging.
const VERBOSE_LOGGING_ENABLED = false; // Toggle true for debug logs
export function logVerbose(...args) { /* ... console.log if enabled ... */ }
export function warnVerbose(...args) { /* ... console.warn if enabled ... */ }
The Todo.txt Webapp provides a functional task management interface using standard web technologies and libraries. It employs localStorage
for local persistence and a service worker for offline use (PWA features). The key enhancement is the robust Dropbox integration, enabling cloud synchronization with conflict detection and resolution. The modular JavaScript codebase effectively separates concerns, isolating UI logic, storage management, event handling, and the complexities of the Dropbox API interaction.