gitshark

Clone repository

git clone https://git.vilip.de/git/vilip/taskflow.git
git clone git@git.vilip.de:vilip/taskflow.git
/**
 * Storage Service
 * Handles local data persistence using AsyncStorage
 * Data is stored as JSON for human readability and portability
 */

import AsyncStorage from '@react-native-async-storage/async-storage';
import { Paths, File } from 'expo-file-system';
import * as Sharing from 'expo-sharing';
import { GTDData, Task, Project, Tag, SyncMetadata } from '../types/gtd';

const STORAGE_KEY = '@taskflow_gtd_data';
const STORAGE_VERSION = '1.0.0';

class StorageService {
  private cache: GTDData | null = null;

  /**
   * Initialize storage with empty data structure
   */
  private getEmptyData(): GTDData {
    return {
      tasks: [],
      projects: [],
      tags: [],
      version: STORAGE_VERSION,
      lastSync: undefined,
      syncHash: undefined,
    };
  }

  /**
   * Load all data from storage
   */
  async loadData(): Promise<GTDData> {
    try {
      const jsonData = await AsyncStorage.getItem(STORAGE_KEY);
      
      if (!jsonData) {
        const emptyData = this.getEmptyData();
        await this.saveData(emptyData);
        this.cache = emptyData;
        return emptyData;
      }

      const data: GTDData = JSON.parse(jsonData);
      this.cache = data;
      return data;
    } catch (error) {
      console.error('Error loading data:', error);
      throw new Error('Failed to load data from storage');
    }
  }

  /**
   * Save all data to storage
   */
  async saveData(data: GTDData): Promise<void> {
    try {
      data.version = STORAGE_VERSION;
      const jsonData = JSON.stringify(data, null, 2);
      await AsyncStorage.setItem(STORAGE_KEY, jsonData);
      this.cache = data;
    } catch (error) {
      console.error('Error saving data:', error);
      throw new Error('Failed to save data to storage');
    }
  }

  /**
   * Get current data (from cache if available)
   */
  async getData(): Promise<GTDData> {
    if (this.cache) {
      return this.cache;
    }
    return this.loadData();
  }

  /**
   * Export data as JSON file and allow sharing/downloading
   */
  async exportToFile(): Promise<string> {
    try {
      const data = await this.getData();
      const jsonData = JSON.stringify(data, null, 2);
      
      const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
      const filename = `taskflow-export-${timestamp}.json`;
      const file = new File(Paths.cache, filename);
      
      await file.write(jsonData);
      
      const isSharingAvailable = await Sharing.isAvailableAsync();
      if (isSharingAvailable) {
        await Sharing.shareAsync(file.uri, {
          mimeType: 'application/json',
          dialogTitle: 'Export Taskflow Data',
          UTI: 'public.json',
        });
      }
      
      return file.uri;
    } catch (error) {
      console.error('Error exporting data:', error);
      throw new Error('Failed to export data');
    }
  }

  /**
   * Import data from JSON string
   */
  async importFromJson(jsonString: string): Promise<void> {
    try {
      const data: GTDData = JSON.parse(jsonString);
      
      // Validate basic structure
      if (!data.tasks || !data.projects || !data.tags) {
        throw new Error('Invalid data format');
      }
      
      // Ensure version is set
      data.version = data.version || STORAGE_VERSION;
      
      await this.saveData(data);
    } catch (error) {
      console.error('Error importing data:', error);
      throw new Error('Failed to import data');
    }
  }

  /**
   * Import data from a file URI
   */
  async importFromFile(fileUri: string): Promise<void> {
    try {
      const file = new File(fileUri);
      const content = await file.text();
      await this.importFromJson(content);
    } catch (error) {
      console.error('Error importing from file:', error);
      throw new Error('Failed to import data from file');
    }
  }

  /**
   * Calculate hash for sync conflict detection
   */
  private calculateHash(data: GTDData): string {
    const sortedData = {
      tasks: [...data.tasks].sort((a, b) => a.id.localeCompare(b.id)),
      projects: [...data.projects].sort((a, b) => a.id.localeCompare(b.id)),
      tags: [...data.tags].sort((a, b) => a.id.localeCompare(b.id)),
    };
    
    const str = JSON.stringify(sortedData);
    let hash = 0;
    for (let i = 0; i < str.length; i++) {
      const char = str.charCodeAt(i);
      hash = ((hash << 5) - hash) + char;
      hash = hash & hash;
    }
    return hash.toString(36);
  }

  /**
   * Prepare sync metadata
   */
  async getSyncMetadata(): Promise<SyncMetadata> {
    const data = await this.getData();
    const hash = this.calculateHash(data);
    
    return {
      lastSyncTimestamp: data.lastSync || 0,
      syncHash: hash,
      conflictDetected: false,
    };
  }

  /**
   * Update sync metadata after successful sync
   */
  async updateSyncMetadata(): Promise<void> {
    const data = await this.getData();
    const hash = this.calculateHash(data);
    
    data.lastSync = Date.now();
    data.syncHash = hash;
    
    await this.saveData(data);
  }

  /**
   * Clear all data (for testing/reset)
   */
  async clearAll(): Promise<void> {
    try {
      await AsyncStorage.removeItem(STORAGE_KEY);
      this.cache = null;
    } catch (error) {
      console.error('Error clearing data:', error);
      throw new Error('Failed to clear data');
    }
  }

  /**
   * Check if conflict exists with remote data
   */
  async detectConflict(remoteHash: string): Promise<boolean> {
    const localMetadata = await this.getSyncMetadata();
    return localMetadata.syncHash !== remoteHash;
  }
}

export default new StorageService();

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog