import { writable } from 'svelte/store'; import { env } from '$env/dynamic/public'; export const isGoogleApiReady = writable(false); export const isSignedIn = writable(false); let tokenClient: google.accounts.oauth2.TokenClient; const TOKEN_KEY = 'google_oauth_token'; export function initGoogleClient(callback: () => void) { const script = document.createElement('script'); script.src = 'https://apis.google.com/js/api.js'; script.onload = () => { gapi.load('client', async () => { await gapi.client.init({ discoveryDocs: [ 'https://www.googleapis.com/discovery/v1/apis/drive/v3/rest', 'https://www.googleapis.com/discovery/v1/apis/sheets/v4/rest', ], }); isGoogleApiReady.set(true); // Restore token from storage if available const saved = localStorage.getItem(TOKEN_KEY); if (saved) { try { const data = JSON.parse(saved); if (data.access_token && data.expires_at && data.expires_at > Date.now()) { gapi.client.setToken({ access_token: data.access_token }); isSignedIn.set(true); } else { localStorage.removeItem(TOKEN_KEY); } } catch { localStorage.removeItem(TOKEN_KEY); } } callback(); }); }; document.body.appendChild(script); const scriptGsi = document.createElement('script'); scriptGsi.src = 'https://accounts.google.com/gsi/client'; scriptGsi.onload = () => { const clientId = env.PUBLIC_GOOGLE_CLIENT_ID; if (!clientId) { console.error('PUBLIC_GOOGLE_CLIENT_ID is not set in the environment.'); return; } tokenClient = google.accounts.oauth2.initTokenClient({ client_id: clientId, scope: 'https://www.googleapis.com/auth/drive.readonly https://www.googleapis.com/auth/spreadsheets.readonly', callback: (tokenResponse) => { if (tokenResponse?.access_token) { // Set token in gapi client gapi.client.setToken({ access_token: tokenResponse.access_token }); isSignedIn.set(true); // Persist token with expiration const expiresInSeconds = tokenResponse.expires_in ? Number(tokenResponse.expires_in) : 0; const expiresInMs = expiresInSeconds * 1000; const record = { access_token: tokenResponse.access_token, expires_at: expiresInMs ? Date.now() + expiresInMs : Date.now() + 3600 * 1000 }; localStorage.setItem(TOKEN_KEY, JSON.stringify(record)); } }, }); }; document.body.appendChild(scriptGsi); } export function handleSignIn() { if (gapi.client.getToken() === null) { tokenClient.requestAccessToken({ prompt: 'consent' }); } else { tokenClient.requestAccessToken({ prompt: '' }); } } export function handleSignOut() { const token = gapi.client.getToken(); if (token !== null) { google.accounts.oauth2.revoke(token.access_token, () => { gapi.client.setToken(null); isSignedIn.set(false); }); } } export async function searchSheets(query: string) { if (!gapi.client.drive) { throw new Error('Google Drive API not loaded'); } const response = await gapi.client.drive.files.list({ q: `mimeType='application/vnd.google-apps.spreadsheet' and name contains '${query}'`, fields: 'files(id, name, iconLink, webViewLink)', pageSize: 20, }); return response.result.files || []; } export async function getSheetNames(spreadsheetId: string) { if (!gapi.client.sheets) { throw new Error('Google Sheets API not loaded'); } const response = await gapi.client.sheets.spreadsheets.get({ spreadsheetId, fields: 'sheets.properties' }); if (!response.result.sheets) { return []; } return response.result.sheets.map(sheet => sheet.properties?.title || ''); } export async function getSheetData(spreadsheetId: string, range: string) { if (!gapi.client.sheets) { throw new Error('Google Sheets API not loaded'); } const response = await gapi.client.sheets.spreadsheets.values.get({ spreadsheetId, range, }); return response.result.values || []; } // Extract Google Drive file ID from various URL formats export function extractDriveFileId(url: string): string | null { if (!url) return null; // Handle different Google Drive URL formats const patterns = [ /\/file\/d\/([a-zA-Z0-9-_]+)/, // https://drive.google.com/file/d/FILE_ID/view /id=([a-zA-Z0-9-_]+)/, // https://drive.google.com/open?id=FILE_ID /\/d\/([a-zA-Z0-9-_]+)/, // https://drive.google.com/uc?id=FILE_ID&export=download /^([a-zA-Z0-9-_]{25,})$/ // Direct file ID ]; for (const pattern of patterns) { const match = pattern.exec(url); if (match) { return match[1]; } } return null; } // Check if URL is a Google Drive URL export function isGoogleDriveUrl(url: string): boolean { return url.includes('drive.google.com') || url.includes('googleapis.com'); } // Download image from Google Drive using the API export async function downloadDriveImage(url: string): Promise { const fileId = extractDriveFileId(url); if (!fileId) { throw new Error('Could not extract file ID from Google Drive URL'); } if (!gapi.client.drive) { throw new Error('Google Drive API not loaded'); } try { // Get file metadata first to check if it exists and is accessible const metadata = await gapi.client.drive.files.get({ fileId: fileId, fields: 'id,name,mimeType,size' }); if (!metadata.result.mimeType?.startsWith('image/')) { throw new Error('File is not an image'); } // Download the file content const response = await gapi.client.drive.files.get({ fileId: fileId, alt: 'media' }); // The response body is already binary data, convert to blob const binaryString = response.body; const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } return new Blob([bytes], { type: metadata.result.mimeType }); } catch (error) { console.error('Error downloading from Google Drive:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; throw new Error(`Failed to download image from Google Drive: ${errorMessage}`); } } // Create an object URL from image data for display export function createImageObjectUrl(blob: Blob): string { return URL.createObjectURL(blob); }