Todo.txt tasks list

Todo.txt tasks list
HTML5 Application

Html5 Applications - Todo.txt

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.

HTML Structure (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:

  • Header: Title, sync status (#syncStatusIndicator), Dropbox button (#dropboxAuthButton).
  • Input Area: Dropdowns (#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).
  • List: Task container (#todo-list).
  • Modal: Conflict resolution dialog (#conflictModal).
  • Scripts: Libraries (jQuery, Bootstrap, Datepicker, Dropbox SDK, jstodotxt) and application modules (dropbox-sync.js, todo.js, todo-datepicker.js, etc.).

CSS Styling (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:

  • Applies a dark theme background to the list and items.
  • Uses flexbox for list item layout.
  • Includes styles for the new dropdowns, date pickers, and the Dropbox button.
  • Handles responsive adjustments for element rounding and datepicker positioning.
  • Styles for completed tasks and priority colors are applied dynamically via JavaScript.

JavaScript Functionality

The application logic is modularized into several JavaScript files within assets/js/apps/sandbox/todotxt/, including dedicated modules for Dropbox integration.

Core Logic & Actions (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
});
  1. Initialization: Imports necessary functions, including initializeDropboxSync. Sets up dropdowns, loads initial todos, and starts the Dropbox sync process on page load.
  2. Core Actions: Defines toggleTodoCompletion, startEditTodo, and deleteTodoItem, updated to handle new UI elements (date pickers, dropdowns) and task properties (completion date).
  3. Dependencies: Relies on other modules for storage, loading, UI updates, and event handling.

Storage Management (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
  // ...
}
  1. Local Timestamp: Tracks the last local save time using localStorage key todosLastModifiedLocal.
  2. Trigger Uploads: Calls uploadTodosToDropbox() after any successful local modification (add, update, remove) to keep Dropbox synchronized.

Loading and Display (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);
}
  1. Loading (todo-load.js): Fetches from storage, parses, sorts, clears the UI, renders items using todo-list-display.js, and updates dropdowns.
  2. UI Helpers (todo-ui.js): Provides utilities for styling and element creation.
  3. Rendering (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.

Event Handling (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) ...
  });
});
  1. Add/Save Logic: Handles adding/editing, incorporating values from dropdowns and date pickers. Constructs the final todo string before saving to storage (which triggers uploads). Resets all relevant UI components.
  2. Filter/Copy: Implements filtering based on dropdown selections and copying all sorted tasks to the clipboard.

Import Functionality (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) ...
  }
});
  1. UI Interaction: Shows/hides the import textarea.
  2. Processing: Splits pasted text, validates lines, adds valid ones using addTodoToStorage (which triggers uploads), and reloads the list.

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) ...
  });
}
  1. Updating: Populates dropdown menus with unique project/context options based on current tasks.
  2. Handling: Uses event delegation to update the button text and hidden input value when a dropdown item is clicked.

Datepicker (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) ...
    });
  });
});
  1. Initialization: Sets up Bootstrap Datepicker on relevant input fields.
  2. Configuration: Customizes icons and handles dynamic class application for positioning via CSS.

Caching (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(/* ... */);
});
  1. Service Worker: Caches all app assets (HTML, CSS, JS including Dropbox modules, images). Serves from cache first for offline use. Handles cache cleanup and updates.
  2. Cache Update: Checks an online version file against a local version and tells the service worker to update its cache if needed.

Dropbox Integration

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.

Overview & Initialization (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
  • Coordinates initialization: Auth first, then offline listeners.
  • Exports uploadTodosToDropbox for triggering uploads from the storage module.

Configuration (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';

Authentication (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 ... */ }
  • Handles obtaining, storing, and clearing the Dropbox access token via OAuth2 redirect flow.
  • Initializes the core Dropbox API (initializeDropboxApi) upon successful authentication.

API Interaction & Sync Logic (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 ... */ }
}
  • Uses the Dropbox.Dropbox SDK client.
  • Provides functions for metadata retrieval, downloading, and uploading 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.
  • Uses dynamic import() to manage dependencies.

Offline Handling (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) ...
}
  • Uses localStorage (PENDING_UPLOAD_KEY) to track if uploads were attempted while offline.
  • Listens for online/offline events.
  • When coming online, if logged in and an upload is pending, it attempts the upload.

UI Feedback (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);
});
  • Provides updateSyncIndicator to give visual feedback on sync state.
  • Includes showConflictModal to handle user interaction during sync conflicts, returning a Promise.
  • Manages the state and behavior of the Dropbox authentication button (updateAuthButton).

Logging (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 ... */ }

Conclusion

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.

Go to the top of the page