From bb6bd0f4b368621f61f50b0f349fd2e350702704 Mon Sep 17 00:00:00 2001 From: mudabbir-ahmad Date: Tue, 14 Apr 2026 19:05:22 +0100 Subject: [PATCH] refactor: clean up comments and unused code in various files --- App.js | 6 -- ServerFiles/server.js | 43 +------------ TEST/testGooglePhotosAPI.js | 22 ------- TEST/testNASAPI.js | 25 -------- app.json | 1 + index.js | 8 --- package.json | 2 +- src/API/AndroidAPI.js | 5 -- src/API/IOSAPI.js | 1 - src/API/NASAPI.js | 5 +- src/AUTH/AuthServiceBase.js | 1 - src/AUTH/GoogleAUTH.js | 78 +----------------------- src/AUTH/IOSAUTH.js | 1 - src/AUTH/NASAUTH.js | 1 - src/components/MediaViewer.js | 24 +------- src/components/PhotoGrid.js | 2 - src/components/ServiceHealthIndicator.js | 1 - src/screens/AlbumsScreen.js | 7 --- src/screens/HomeScreen.js | 33 ---------- src/screens/LoginScreen.js | 2 - src/screens/OnboardingScreen.js | 15 ----- src/screens/RegisterScreen.js | 2 - src/screens/SettingsScreen.js | 26 +------- src/services/CredentialEncryption.js | 15 ----- src/services/DatabaseService.js | 29 +-------- src/services/DeviceMediaService.js | 28 +-------- src/services/ErrorHandler.js | 21 ------- src/services/ThemeContext.js | 2 - 28 files changed, 14 insertions(+), 392 deletions(-) delete mode 100644 index.js diff --git a/App.js b/App.js index 4efbe65..c9e9006 100644 --- a/App.js +++ b/App.js @@ -14,14 +14,10 @@ import HomeScreen from './src/screens/HomeScreen'; import SearchScreen from './src/screens/SearchScreen'; import CollectionsScreen from './src/screens/AlbumsScreen'; import SettingsScreen from './src/screens/SettingsScreen'; - -// Required for OAuth deep link redirect to close the browser and return to the app WebBrowser.maybeCompleteAuthSession(); const Stack = createNativeStackNavigator(); const Tab = createBottomTabNavigator(); - -// Bottom tab navigator shown after login function MainTabs({ route }) { const { userId } = route.params || {}; const { theme } = useTheme(); @@ -73,8 +69,6 @@ function MainTabs({ route }) { ); } - -// Root stack navigator function AppNavigator() { return ( diff --git a/ServerFiles/server.js b/ServerFiles/server.js index 9bc3045..3cda39c 100644 --- a/ServerFiles/server.js +++ b/ServerFiles/server.js @@ -14,17 +14,12 @@ const execFileAsync = util.promisify(execFile); const SSH_OPTIONS = ['-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null']; -// Read the default NAS photo directory from the config file const NAS_CONFIG_PATH = path.join(__dirname, 'nas_config.txt'); let DEFAULT_PHOTO_DIR = '/home/user/photos'; try { const raw = fs.readFileSync(NAS_CONFIG_PATH, 'utf8').trim(); if (raw) DEFAULT_PHOTO_DIR = raw.split('\n')[0].trim(); -} catch (_) { - // Config file missing — keep the hard-coded default -} - -// SSH/SFTP shell helpers +} catch (_) {} function runCommand(cmd, args, options = {}) { return execFileAsync(cmd, args, { @@ -66,8 +61,6 @@ async function runSftpBatch(conn, batchCommands) { } } -// Express setup - const app = express(); app.use(express.json()); app.use(express.urlencoded({ extended: true })); @@ -82,8 +75,6 @@ app.use((req, res, next) => { next(); }); -// JWT verification middleware - function verifyToken(req, res, next) { const authHeader = req.headers.authorization; const token = authHeader?.split(' ')[1]; @@ -100,10 +91,6 @@ function verifyToken(req, res, next) { } } -// ─── REST Endpoints ────────────────────────────────────────────── - -// Health check - app.get('/api/health', (req, res) => { res.json({ status: 'ok', @@ -113,14 +100,10 @@ app.get('/api/health', (req, res) => { }); }); -// Return the current NAS photo directory read from nas_config.txt - app.get('/api/config/photo-dir', (req, res) => { res.json({ photoDir: DEFAULT_PHOTO_DIR }); }); -// Establish an SFTP connection (stores credentials, returns a JWT) - app.post('/api/sftp/connect', (req, res) => { const { host, port = 22, username, password } = req.body; if (!host || !username || !password) { @@ -141,8 +124,6 @@ app.post('/api/sftp/connect', (req, res) => { }); }); -// List directory contents - app.post('/api/sftp/list', verifyToken, async (req, res) => { const { path: dirPath = DEFAULT_PHOTO_DIR } = req.body; const conn = req.connection; @@ -170,8 +151,6 @@ app.post('/api/sftp/list', verifyToken, async (req, res) => { } }); -// Download a file from the NAS - app.get('/api/sftp/download', verifyToken, async (req, res) => { const { path: filePath } = req.query; if (!filePath) return res.status(400).json({ error: 'Missing path parameter' }); @@ -183,7 +162,7 @@ app.get('/api/sftp/download', verifyToken, async (req, res) => { try { await runCommand('scp', buildScpArgs(conn, `${conn.username}@${conn.host}:${filePath}`, tempFilePath)); res.download(tempFilePath, filename, () => { - try { fs.unlinkSync(tempFilePath); } catch (_) { /* already cleaned */ } + try { fs.unlinkSync(tempFilePath); } catch (_) {} }); } catch (err) { console.error('Download error:', err.message); @@ -191,8 +170,6 @@ app.get('/api/sftp/download', verifyToken, async (req, res) => { } }); -// Upload a file to the NAS - app.post('/api/sftp/upload', verifyToken, async (req, res) => { const { remotePath } = req.body; if (!remotePath || !req.files?.file) { @@ -218,8 +195,6 @@ app.post('/api/sftp/upload', verifyToken, async (req, res) => { } }); -// Create a directory on the NAS - app.post('/api/sftp/mkdir', verifyToken, async (req, res) => { const { path: dirPath } = req.body; if (!dirPath) return res.status(400).json({ error: 'Missing path parameter' }); @@ -234,8 +209,6 @@ app.post('/api/sftp/mkdir', verifyToken, async (req, res) => { } }); -// Delete a file on the NAS - app.post('/api/sftp/delete', verifyToken, async (req, res) => { const { path: filePath } = req.body; if (!filePath) return res.status(400).json({ error: 'Missing path parameter' }); @@ -250,18 +223,12 @@ app.post('/api/sftp/delete', verifyToken, async (req, res) => { } }); -// Disconnect from the NAS - app.post('/api/sftp/disconnect', verifyToken, (req, res) => { connections.delete(req.connectionId); console.log(`Disconnected: ${req.connectionId}`); res.json({ success: true, message: 'Disconnected from SFTP server' }); }); -// ─── Google Photos → NAS Proxy Endpoints ───────────────────────── - -// Fetch media items from Google Photos on behalf of the mobile client - app.post('/api/google-photos/fetch', verifyToken, async (req, res) => { const { googleAccessToken, pageSize = 50, pageToken } = req.body; if (!googleAccessToken) { @@ -302,8 +269,6 @@ app.post('/api/google-photos/fetch', verifyToken, async (req, res) => { } }); -// Download a Google Photos item and save it directly to the NAS - app.post('/api/google-photos/download-to-nas', verifyToken, async (req, res) => { const { googleAccessToken, baseUrl, filename, remotePath } = req.body; const conn = req.connection; @@ -337,14 +302,10 @@ app.post('/api/google-photos/download-to-nas', verifyToken, async (req, res) => } }); -// Sync status (placeholder for future batch sync) - app.post('/api/google-photos/sync-status', verifyToken, (req, res) => { res.json({ success: true, status: 'idle', message: 'No active sync in progress', lastSync: null }); }); -// ─── Lifecycle ─────────────────────────────────────────────────── - process.on('SIGINT', () => { console.log('\nShutting down...'); connections.forEach((_, id) => connections.delete(id)); diff --git a/TEST/testGooglePhotosAPI.js b/TEST/testGooglePhotosAPI.js index 051b921..f718669 100644 --- a/TEST/testGooglePhotosAPI.js +++ b/TEST/testGooglePhotosAPI.js @@ -1,21 +1,3 @@ -// Google Photos Picker API Test -// -// Usage: -// node TEST/testGooglePhotosAPI.js -// GOOGLE_ACCESS_TOKEN=ya29.xxx node TEST/testGooglePhotosAPI.js -// -// The token must have been granted the scope: -// https://www.googleapis.com/auth/photospicker.mediaitems.readonly -// -// Getting a token from OAuth Playground (https://developers.google.com/oauthplayground): -// 1. Click the gear icon → check "Use your own OAuth credentials" -// 2. Enter your Client ID and Client Secret from Google Cloud Console -// 3. In "Input your own scopes" enter: -// https://www.googleapis.com/auth/photospicker.mediaitems.readonly -// 4. Click "Authorize APIs" and sign in with your test Google account -// 5. Click "Exchange authorization code for tokens" -// 6. Copy the Access token (starts with ya29.) and pass it here - const PICKER_BASE = 'https://photospicker.googleapis.com/v1'; const USERINFO_URL = 'https://www.googleapis.com/oauth2/v2/userinfo'; @@ -37,7 +19,6 @@ async function main() { console.log('\n--- Google Photos Picker API Test ---\n'); - // Test 1 — User info console.log('[1] Fetching user info...'); try { const res = await fetch(USERINFO_URL, { headers }); @@ -51,7 +32,6 @@ async function main() { console.log(` Failed: ${err.message}\n`); } - // Test 2 — Create Picker session console.log('[2] Creating Picker session...'); let sessionId = null; try { @@ -68,7 +48,6 @@ async function main() { console.log(` Session created: ${sessionId}`); console.log(` mediaItemsSet: ${session.mediaItemsSet}\n`); - // Wait for session to be ready if (!session.mediaItemsSet) { console.log(' Waiting for session to become ready...'); for (let i = 0; i < 10; i++) { @@ -87,7 +66,6 @@ async function main() { console.log(` Failed: ${err.message}\n`); } - // Test 3 — List media items from session if (sessionId) { console.log('[3] Fetching media items from session...'); try { diff --git a/TEST/testNASAPI.js b/TEST/testNASAPI.js index 716b9af..357092c 100644 --- a/TEST/testNASAPI.js +++ b/TEST/testNASAPI.js @@ -1,31 +1,9 @@ -/** - * NAS Backend API Test - * - * Usage: - * node TEST/testNASAPI.js - * - * Environment variables: - * BACKEND_URL (default http://localhost:3001) - * NAS_HOST e.g. 192.168.1.100 - * NAS_PORT default 22 - * NAS_USER SSH username - * NAS_PASS SSH password - * NAS_FOLDER default /home/user/photos - * - * Tests: - * 1. Backend health check - * 2. SFTP connect - * 3. List directory - * 4. Disconnect - */ - const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:3001'; async function main() { console.log('\n--- NAS Backend API Test ---'); console.log(`Backend: ${BACKEND_URL}\n`); - // 1 — Health check console.log('[1] Health check...'); try { const res = await fetch(`${BACKEND_URL}/api/health`); @@ -50,7 +28,6 @@ async function main() { return; } - // 2 — Connect let connectionToken = null; console.log(`[2] SFTP connect to ${host}:${port}...`); try { @@ -74,7 +51,6 @@ async function main() { Authorization: `Bearer ${connectionToken}`, }; - // 3 — List directory console.log(`[3] List directory "${folder}"...`); try { const res = await fetch(`${BACKEND_URL}/api/sftp/list`, { @@ -93,7 +69,6 @@ async function main() { console.log(` Failed: ${err.message}\n`); } - // 4 — Disconnect console.log('[4] Disconnect...'); try { const res = await fetch(`${BACKEND_URL}/api/sftp/disconnect`, { diff --git a/app.json b/app.json index bbbcd66..7f7c250 100644 --- a/app.json +++ b/app.json @@ -1,5 +1,6 @@ { "expo": { + "entryPoint": "./App.js", "name": "MASS Photo App", "slug": "mass-expo", "owner": "bobthebob", diff --git a/index.js b/index.js deleted file mode 100644 index 1d6e981..0000000 --- a/index.js +++ /dev/null @@ -1,8 +0,0 @@ -import { registerRootComponent } from 'expo'; - -import App from './App'; - -// registerRootComponent calls AppRegistry.registerComponent('main', () => App); -// It also ensures that whether you load the app in Expo Go or in a native build, -// the environment is set up appropriately -registerRootComponent(App); diff --git a/package.json b/package.json index e644171..f6c5e70 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "media-aggregation-and-sorting-system", "version": "1.0.0", - "main": "index.js", + "main": "App.js", "scripts": { "start": "expo start", "android": "expo run:android", diff --git a/src/API/AndroidAPI.js b/src/API/AndroidAPI.js index 36e2120..3ce29c5 100644 --- a/src/API/AndroidAPI.js +++ b/src/API/AndroidAPI.js @@ -1,5 +1,3 @@ -// Google Photos Picker API wrapper — replaces the deprecated Photos Library API. -// Uses sessions to access user photos from Google Photos cloud. const GooglePhotosAPI = (getAccessToken) => { const PICKER_ROOT = 'https://photospicker.googleapis.com/v1'; @@ -29,13 +27,10 @@ const GooglePhotosAPI = (getAccessToken) => { }; return { - // Create a Picker session — must be called before listing items createSession: () => callFetch(`${PICKER_ROOT}/sessions`, 'POST', {}), - // Poll session status getSession: (sessionId) => callFetch(`${PICKER_ROOT}/sessions/${sessionId}`, 'GET'), - // List media items from a ready session listMediaItems: (sessionId, pageSize = 25, pageToken) => { const params = new URLSearchParams({ sessionId, pageSize: String(pageSize) }); if (pageToken) params.append('pageToken', pageToken); diff --git a/src/API/IOSAPI.js b/src/API/IOSAPI.js index 4f05695..050bef8 100644 --- a/src/API/IOSAPI.js +++ b/src/API/IOSAPI.js @@ -1,4 +1,3 @@ -// iCloud Photos API placeholder — Apple does not provide a public REST API for iCloud Photos const iCloudPhotosAPI = () => ({ listMediaItems: async () => ({ isSuccess: false, message: 'iCloud API not available' }), getMediaItem: async () => ({ isSuccess: false, message: 'iCloud API not available' }), diff --git a/src/API/NASAPI.js b/src/API/NASAPI.js index 8707648..867d099 100644 --- a/src/API/NASAPI.js +++ b/src/API/NASAPI.js @@ -1,6 +1,5 @@ import axios from 'axios'; -// HTTP client that communicates with the SFTP backend (ServerFiles/server.js) const DEFAULT_BACKEND_URL = 'http://192.168.1.100:3001'; class NASAPI { @@ -11,7 +10,6 @@ class NASAPI { this.credentials = null; } - // Establish a connection via the backend; falls back to local-only config if the backend is unreachable async connect(host, port, username, password, photoFolder = '/Photos') { if (!host || !username || !password) throw new Error('Host, username, and password are required'); @@ -50,7 +48,6 @@ class NASAPI { return response.data; } - // Filter directory listing for supported photo/video files async getPhotos(folder = null) { const targetFolder = folder || this.credentials?.photoFolder || '/Photos'; if (!this.connectionToken) return { mediaItems: [], folder: targetFolder, message: 'Backend not connected' }; @@ -120,7 +117,7 @@ class NASAPI { if (this.connectionToken) { try { await axios.post(`${this.backendUrl}/api/sftp/disconnect`, {}, { headers: this.getHeaders(), timeout: 5000 }); - } catch (_) { /* ignore */ } + } catch (_) {} } this.connectionToken = null; this.isConnected = false; diff --git a/src/AUTH/AuthServiceBase.js b/src/AUTH/AuthServiceBase.js index cd244e0..58d57c2 100644 --- a/src/AUTH/AuthServiceBase.js +++ b/src/AUTH/AuthServiceBase.js @@ -1,4 +1,3 @@ -// Abstract base class for authentication services — provides a common interface for all providers export class AuthServiceBase { constructor() { if (new.target === AuthServiceBase) { diff --git a/src/AUTH/GoogleAUTH.js b/src/AUTH/GoogleAUTH.js index 2b3e541..25102c5 100644 --- a/src/AUTH/GoogleAUTH.js +++ b/src/AUTH/GoogleAUTH.js @@ -7,11 +7,8 @@ import { logError } from '../services/ErrorHandler'; WebBrowser.maybeCompleteAuthSession(); -// Android client ID — created in Google Cloud Console as "Android" type -// with package name com.bobthebob.massphotoapp and the SHA-1 from -// `eas credentials --platform android`. const ANDROID_CLIENT_ID = '313090284964-018uf4v1bmmrra09lvmpnuug2qs09047.apps.googleusercontent.com'; -const IOS_CLIENT_ID = '313090284964-luka809nuc66bb4cg4gameovtqg8uoj1.apps.googleusercontent.com'; // Optional, just in case being kept here. +const IOS_CLIENT_ID = '313090284964-luka809nuc66bb4cg4gameovtqg8uoj1.apps.googleusercontent.com'; const GOOGLE_DISCOVERY = { authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth', @@ -19,9 +16,6 @@ const GOOGLE_DISCOVERY = { revocationEndpoint: 'https://oauth2.googleapis.com/revoke', }; -// The old Photos Library API (photoslibrary.googleapis.com) was shut down -// in 2025. Google's replacement is the Photos Picker API which uses its -// own scope. const SCOPES = [ 'openid', 'https://www.googleapis.com/auth/userinfo.email', @@ -44,8 +38,6 @@ class GoogleAUTH extends AuthServiceBase { this.email = null; } - // ───────────────────── Authentication ───────────────────── - async authenticate() { if (ANDROID_CLIENT_ID === 'REPLACE_WITH_ANDROID_CLIENT_ID.apps.googleusercontent.com') { console.warn('Google Auth: no Android client ID — using placeholder mode'); @@ -188,23 +180,6 @@ class GoogleAUTH extends AuthServiceBase { return info || { id: 'google_user', email: this.email || 'unknown', name: 'Google User' }; } - // ───────────────── Photos Picker API ────────────────── - // - // The Picker API is interactive — the user must open the pickerUri in a - // browser to select which photos to share with the app. Once the user - // finishes selecting, mediaItemsSet becomes true and we can fetch items. - // - // Flow: - // 1. POST /v1/sessions → get { id, pickerUri } - // 2. Open pickerUri in browser → user selects photos → browser closes - // 3. Poll GET /v1/sessions/{id} until mediaItemsSet = true - // 4. GET /v1/mediaItems?sessionId={id}&pageSize=N → selected items - - /** - * Create a new Picker session. - * Returns the full session object { id, pickerUri, mediaItemsSet, ... } - * or { error: { code, message } } on failure. - */ async _createPickerSession() { const token = await this.getAccessToken(); const res = await fetch(`${PICKER_API}/sessions`, { @@ -225,10 +200,6 @@ class GoogleAUTH extends AuthServiceBase { return await res.json(); } - /** - * Poll a session until mediaItemsSet becomes true. - * Waits up to ~30 seconds (user needs time to select photos). - */ async _waitForSession(sessionId) { const token = await this.getAccessToken(); const maxAttempts = 20; @@ -254,9 +225,6 @@ class GoogleAUTH extends AuthServiceBase { return null; } - /** - * Fetch media items from a ready Picker session. - */ async _fetchPickerMediaItems(sessionId, pageSize = 100, pageToken = null) { const token = await this.getAccessToken(); const params = new URLSearchParams({ @@ -277,7 +245,6 @@ class GoogleAUTH extends AuthServiceBase { const data = await res.json(); - // Debug: log raw response structure if (data.mediaItems?.length) { console.log('Picker raw item[0]:', JSON.stringify(data.mediaItems[0], null, 2)); } else { @@ -287,19 +254,10 @@ class GoogleAUTH extends AuthServiceBase { return data; } - /** - * Normalise Picker API media items to the app's standard format. - * - * The Picker API response shape per item: - * { id, mediaFile: { baseUrl, mimeType, filename, mediaFileMetadata: { ... } } } - * - * baseUrl requires an Authorization header to fetch the actual image bytes. - */ _normalisePickerItems(items) { if (!items?.length) return []; return items.map((item, idx) => { - // The item may have mediaFile nested, or may be flat const file = item.mediaFile || item; const baseUrl = file.baseUrl || item.baseUrl || null; const filename = file.filename || item.filename || 'Untitled'; @@ -328,24 +286,12 @@ class GoogleAUTH extends AuthServiceBase { }); } - /** - * Download Picker images to local cache so can render them. - * - * The Picker API's baseUrl requires an Authorization header. React - * Native's can't add custom headers reliably, so we download - * each photo to a cache file and set local_path to the file:// URI. - * - * @param {Array} items — normalised items from _normalisePickerItems - * @returns {Array} — same items with local_path populated - */ async downloadPickerPhotos(items) { const token = await this.getAccessToken(); - // Ensure cache directory exists if (!CACHE_DIR.exists) { CACHE_DIR.create({ intermediates: true }); } else { - // Clean up stale files from previous failed downloads (< 1KB = likely error pages) try { const entries = CACHE_DIR.list(); for (const entry of entries) { @@ -353,7 +299,7 @@ class GoogleAUTH extends AuthServiceBase { entry.delete(); } } - } catch (_) { /* ignore cleanup errors */ } + } catch (_) {} } const results = []; @@ -371,14 +317,11 @@ class GoogleAUTH extends AuthServiceBase { : '.jpg'; const destFile = new File(CACHE_DIR, `${safeId}${ext}`); - // Skip download if already cached and non-empty if (destFile.exists && destFile.size > 1024) { results.push({ ...item, local_path: destFile.uri }); continue; } - // The Picker API baseUrl is a direct download link when used with - // the Authorization header. No suffix needed. console.log(`Downloading: ${item.title} (${item.picker_base_url.substring(0, 80)}…)`); const downloaded = await File.downloadFileAsync( @@ -392,19 +335,17 @@ class GoogleAUTH extends AuthServiceBase { if (downloaded.exists && downloaded.size > 1024) { results.push({ ...item, local_path: downloaded.uri }); } else if (downloaded.exists && downloaded.size > 0) { - // Suspiciously small — check if it's an error page try { const peek = await downloaded.text(); if (peek.startsWith('<') || peek.startsWith('{')) { logError('Download Picker Photo', `Got error page for ${item.title}: ${peek.substring(0, 150)}`); downloaded.delete(); - results.push(item); // no local_path + results.push(item); } else { results.push({ ...item, local_path: downloaded.uri }); } } catch (_) { - // Can't read as text = probably binary = probably an image results.push({ ...item, local_path: downloaded.uri }); } } else { @@ -421,15 +362,8 @@ class GoogleAUTH extends AuthServiceBase { return results; } - /** - * Full interactive flow: create session → open picker in browser → - * wait for user to select photos → fetch the selected items. - * - * @returns {{ mediaItems, nextPageToken, sessionId, error? }} - */ async pickPhotos() { try { - // 1. Create session console.log('Creating Google Photos Picker session…'); const session = await this._createPickerSession(); @@ -449,7 +383,6 @@ class GoogleAUTH extends AuthServiceBase { console.log('Picker session created:', session.id); console.log('Opening picker for user selection…'); - // 2. Open the picker URI in a browser for the user to select photos await WebBrowser.openBrowserAsync(session.pickerUri, { dismissButtonStyle: 'done', presentationStyle: 'pageSheet', @@ -457,7 +390,6 @@ class GoogleAUTH extends AuthServiceBase { console.log('Browser closed — polling for selected photos…'); - // 3. Poll until the user's selections are ready const ready = await this._waitForSession(session.id); if (!ready) { return { @@ -466,7 +398,6 @@ class GoogleAUTH extends AuthServiceBase { }; } - // 4. Fetch selected items and download to local cache const result = await this._fetchPickerMediaItems(session.id, 100, null); let mediaItems = this._normalisePickerItems(result.mediaItems); @@ -485,9 +416,6 @@ class GoogleAUTH extends AuthServiceBase { } } - /** - * Fetch more items from an existing Picker session (pagination). - */ async getMorePhotos(pageSize = 100, pageToken, sessionId) { try { if (!sessionId || !pageToken) { diff --git a/src/AUTH/IOSAUTH.js b/src/AUTH/IOSAUTH.js index 4a5109f..af80f51 100644 --- a/src/AUTH/IOSAUTH.js +++ b/src/AUTH/IOSAUTH.js @@ -1,6 +1,5 @@ import { AuthServiceBase } from './AuthServiceBase'; -// iCloud Photos placeholder — Apple does not provide a public iCloud Photos API class IOSAUTH extends AuthServiceBase { constructor() { super(); diff --git a/src/AUTH/NASAUTH.js b/src/AUTH/NASAUTH.js index 6b50d31..c8e777a 100644 --- a/src/AUTH/NASAUTH.js +++ b/src/AUTH/NASAUTH.js @@ -1,7 +1,6 @@ import { AuthServiceBase } from './AuthServiceBase'; import NASAPI from '../API/NASAPI'; -// NAS SFTP authentication — connects to the backend server via NASAPI class NASAUTH extends AuthServiceBase { constructor() { super(); diff --git a/src/components/MediaViewer.js b/src/components/MediaViewer.js index 8575906..214ca92 100644 --- a/src/components/MediaViewer.js +++ b/src/components/MediaViewer.js @@ -13,16 +13,6 @@ const getServiceIcon = (type) => ({ DEVICE: '📱', GOOGLE_PHOTOS: '📷', ICLOUD_PHOTOS: '🍎', NAS: '💾' }[type] || '📱'); const getServiceName = (type) => ({ DEVICE: 'On Device', GOOGLE_PHOTOS: 'Google Photos', ICLOUD_PHOTOS: 'iCloud', NAS: 'NAS' }[type] || type); - -/** - * Fullscreen media viewer with thumbnail carousel. - * - * Props: - * visible – boolean to show/hide the modal - * media – currently selected media item - * allMedia – (optional) full array of media items for carousel navigation - * onClose – callback when user dismisses the viewer - */ const MediaViewer = ({ visible, media, allMedia, onClose }) => { const { theme } = useTheme(); const mainListRef = useRef(null); @@ -33,7 +23,6 @@ const MediaViewer = ({ visible, media, allMedia, onClose }) => { const mediaList = allMedia && allMedia.length > 0 ? allMedia : (media ? [media] : []); const hasCarousel = mediaList.length > 1; - // When the modal opens or media prop changes, scroll to the correct item useEffect(() => { if (!visible || !media || mediaList.length === 0) return; @@ -44,7 +33,6 @@ const MediaViewer = ({ visible, media, allMedia, onClose }) => { setActiveIndex(newIndex); setCurrentMedia(mediaList[newIndex]); - // Delay scroll so FlatList has rendered setTimeout(() => { if (mainListRef.current && newIndex >= 0) { mainListRef.current.scrollToOffset({ offset: newIndex * SCREEN_WIDTH, animated: false }); @@ -52,7 +40,7 @@ const MediaViewer = ({ visible, media, allMedia, onClose }) => { if (thumbListRef.current && newIndex >= 0) { try { thumbListRef.current.scrollToIndex({ index: newIndex, animated: false, viewPosition: 0.5 }); - } catch (e) { /* index might be out of range during initial render */ } + } catch (e) {} } }, 100); }, [visible, media]); @@ -69,7 +57,7 @@ const MediaViewer = ({ visible, media, allMedia, onClose }) => { if (thumbListRef.current) { try { thumbListRef.current.scrollToIndex({ index: idx, animated: true, viewPosition: 0.5 }); - } catch (_) { /* ignore */ } + } catch (_) {} } } }; @@ -120,12 +108,10 @@ const MediaViewer = ({ visible, media, allMedia, onClose }) => { return ( - {/* Close button */} ✕ - {/* Counter */} {hasCarousel && ( @@ -134,7 +120,6 @@ const MediaViewer = ({ visible, media, allMedia, onClose }) => { )} - {/* Main image area – swipeable */} {hasCarousel ? ( { style={styles.mainList} /> ) : ( - // Single image mode (backward compat) displayMedia.local_path ? ( ) : ( @@ -163,7 +147,6 @@ const MediaViewer = ({ visible, media, allMedia, onClose }) => { ) )} - {/* Thumbnail carousel */} {hasCarousel && ( { )} - {/* Info panel */} {displayMedia.title || 'Photo'} @@ -234,7 +216,6 @@ const styles = StyleSheet.create({ mainList: { flex: 1 }, fullImage: { flex: 1, width: '100%' }, fullPlaceholder: { flex: 1, justifyContent: 'center', alignItems: 'center' }, - // Thumbnail carousel thumbCarousel: { position: 'absolute', bottom: 110, left: 0, right: 0, zIndex: 15, backgroundColor: 'rgba(0,0,0,0.4)', @@ -250,7 +231,6 @@ const styles = StyleSheet.create({ width: '100%', height: '100%', backgroundColor: '#333', justifyContent: 'center', alignItems: 'center', }, - // Info panel infoPanel: { padding: 16, paddingBottom: 32, borderTopLeftRadius: 16, borderTopRightRadius: 16, diff --git a/src/components/PhotoGrid.js b/src/components/PhotoGrid.js index c7b252e..3476803 100644 --- a/src/components/PhotoGrid.js +++ b/src/components/PhotoGrid.js @@ -29,7 +29,6 @@ const PhotoGrid = ({ : [item.source_service]; if (sources.length === 1) { - // Single source — simple badge return ( {getServiceIcon(sources[0])} @@ -37,7 +36,6 @@ const PhotoGrid = ({ ); } - // Multiple sources — show all icons with a subtle indicator return ( {sources.map((src, i) => ( diff --git a/src/components/ServiceHealthIndicator.js b/src/components/ServiceHealthIndicator.js index d08c93d..90c9ffd 100644 --- a/src/components/ServiceHealthIndicator.js +++ b/src/components/ServiceHealthIndicator.js @@ -1,7 +1,6 @@ import React from 'react'; import { View, StyleSheet } from 'react-native'; -// Circular health indicator: inactive=grey, healthy=green, error=red, checking=amber const COLORS = { inactive: '#9e9e9e', healthy: '#34a853', diff --git a/src/screens/AlbumsScreen.js b/src/screens/AlbumsScreen.js index 811b1fa..26661fd 100644 --- a/src/screens/AlbumsScreen.js +++ b/src/screens/AlbumsScreen.js @@ -39,7 +39,6 @@ const CollectionsScreen = ({ route }) => { const allMedia = await DatabaseService.getMediaByUser(userId); const sectionList = []; - // ─── Utility albums (top) ─── const utilityAlbums = [ { name: 'All Photos', icon: '📸', count: allMedia.length, media: allMedia, type: 'utility' }, { name: 'Hidden', icon: '🙈', count: 0, media: [], type: 'utility' }, @@ -48,7 +47,6 @@ const CollectionsScreen = ({ route }) => { ]; sectionList.push({ title: '', data: [{ albums: utilityAlbums }] }); - // ─── Per-service categories ─── for (const acc of accounts) { const media = await DatabaseService.getMediaByService(userId, acc.service_type); const serviceAlbums = [ @@ -77,7 +75,6 @@ const CollectionsScreen = ({ route }) => { } }; - // Album detail view if (selectedAlbum) { const gridItemSize = (width - 6) / 3; return ( @@ -209,13 +206,11 @@ const styles = StyleSheet.create({ header: { paddingHorizontal: 16, paddingTop: 52, paddingBottom: 12 }, headerTitle: { fontSize: 24, fontWeight: '700' }, listContent: { paddingHorizontal: ALBUM_GAP, paddingBottom: 20 }, - // Section headers sectionHeader: { flexDirection: 'row', alignItems: 'center', paddingVertical: 12, gap: 8, }, sectionIcon: { fontSize: 20 }, sectionTitle: { fontSize: 16, fontWeight: '700' }, - // Album grid albumRow: { flexDirection: 'row', gap: ALBUM_GAP, marginBottom: ALBUM_GAP }, albumCard: {}, albumCover: { borderRadius: 12, overflow: 'hidden', marginBottom: 8 }, @@ -225,7 +220,6 @@ const styles = StyleSheet.create({ }, albumName: { fontSize: 14, fontWeight: '600', paddingHorizontal: 2 }, albumCount: { fontSize: 12, paddingHorizontal: 2, marginTop: 2 }, - // Album detail albumHeader: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingTop: 52, paddingBottom: 12, gap: 12, borderBottomWidth: 1, @@ -238,7 +232,6 @@ const styles = StyleSheet.create({ gridItem: { margin: 1 }, gridImage: { width: '100%', height: '100%' }, gridPlaceholder: { width: '100%', height: '100%', justifyContent: 'center', alignItems: 'center' }, - // Empty / Loading loadingText: { marginTop: 12, fontSize: 14 }, emptyTitle: { fontSize: 18, fontWeight: '600', marginBottom: 6 }, emptyText: { fontSize: 13, textAlign: 'center' }, diff --git a/src/screens/HomeScreen.js b/src/screens/HomeScreen.js index a615533..70ad2ee 100644 --- a/src/screens/HomeScreen.js +++ b/src/screens/HomeScreen.js @@ -14,16 +14,7 @@ import PhotoGrid from '../components/PhotoGrid'; import MediaViewer from '../components/MediaViewer'; import { logError } from '../services/ErrorHandler'; import { TouchableOpacity } from 'react-native'; - -/** - * Merge media items from different services. - * Items with the same filename (title) are merged into a single entry - * with a `sources` array listing every service it appears in. - * The best local_path is kept (prefer file:// from DEVICE over a cached - * Picker download, since device URIs are always fresh). - */ const mergeMediaAcrossServices = (items) => { - // First pass: group by normalised filename const byFilename = new Map(); const noFilename = []; @@ -36,15 +27,12 @@ const mergeMediaAcrossServices = (items) => { if (byFilename.has(key)) { const existing = byFilename.get(key); - // Add this service to the sources list if (!existing.sources.includes(item.source_service)) { existing.sources.push(item.source_service); } - // Prefer a local device path over a cached download if (item.source_service === 'DEVICE' && item.local_path) { existing.local_path = item.local_path; } - // Keep the best local_path if the existing one is missing if (!existing.local_path && item.local_path) { existing.local_path = item.local_path; } @@ -52,8 +40,6 @@ const mergeMediaAcrossServices = (items) => { byFilename.set(key, { ...item, sources: [item.source_service] }); } } - - // Second pass: also deduplicate by source_id for items with different titles const bySourceId = new Map(); for (const item of [...byFilename.values(), ...noFilename]) { const key = item.source_id || item.id || item.local_path || Math.random().toString(); @@ -118,7 +104,6 @@ const HomeScreen = ({ navigation, route }) => { try { const newMedia = []; - // ─── Device photos (local camera roll, screenshots, etc.) ─── try { const deviceResult = await DeviceMediaService.getPhotos(100); if (deviceResult.mediaItems.length) { @@ -130,11 +115,9 @@ const HomeScreen = ({ navigation, route }) => { console.error('Device media fetch error:', err); } - // ─── Previously imported Google Photos (stored in DB) ─── try { const googleMedia = await DatabaseService.getMediaByService(userId, 'GOOGLE_PHOTOS'); if (googleMedia?.length) { - // Validate cached files still exist — clear stale paths const validated = googleMedia.map((item) => { if (item.local_path) { try { @@ -154,7 +137,6 @@ const HomeScreen = ({ navigation, route }) => { console.error('DB Google media fetch error:', err); } - // ─── NAS fetch ─── const nasAccount = linkedAccounts.find((a) => a.service_type === 'NAS'); if (nasAccount) { try { @@ -169,15 +151,11 @@ const HomeScreen = ({ navigation, route }) => { } } - // ─── iCloud (placeholder) ─── const iosAccount = linkedAccounts.find((a) => a.service_type === 'ICLOUD_PHOTOS'); if (iosAccount) { console.log('iCloud: placeholder — would fetch photos here'); } - // ─── Merge & cross-reference across services ─── - // Match items across sources by filename so we can show "this photo - // exists on Device AND Google Photos" etc. const merged = mergeMediaAcrossServices(newMedia); setAllMedia(merged); } catch (err) { @@ -188,7 +166,6 @@ const HomeScreen = ({ navigation, route }) => { } }; - // ─── Import from Google Photos (opens picker in browser) ─── const importGooglePhotos = async () => { const googleAccount = linkedAccounts.find((a) => a.service_type === 'GOOGLE_PHOTOS'); if (!googleAccount) { @@ -206,7 +183,6 @@ const HomeScreen = ({ navigation, route }) => { let result = await googleAuth.pickPhotos(); - // If 403 (stale scopes), auto re-auth and retry if (result?.error?.code === 403) { console.log('Stale token — re-authenticating…'); await googleAuth.revokeAccess(); @@ -223,7 +199,6 @@ const HomeScreen = ({ navigation, route }) => { if (result?.error) { logError('Import Google Photos', result.error.message); } else if (result?.mediaItems?.length) { - // Save imported items to DB so they persist across app restarts for (const item of result.mediaItems) { await DatabaseService.saveMediaItem(userId, item); } @@ -231,13 +206,11 @@ const HomeScreen = ({ navigation, route }) => { setGoogleNextPage(result.nextPageToken || null); setGoogleSessionId(result.sessionId || null); - // Merge into current view with cross-service matching setAllMedia((prev) => mergeMediaAcrossServices([...prev, ...result.mediaItems])); console.log(`Imported ${result.mediaItems.length} photos from Google`); } - // Persist refreshed tokens if (googleAuth.accessToken && googleAuth.accessToken !== data.accessToken) { await DatabaseService.linkAccount(userId, 'GOOGLE_PHOTOS', { accessToken: googleAuth.accessToken, @@ -256,7 +229,6 @@ const HomeScreen = ({ navigation, route }) => { const filterMedia = () => { let filtered = allMedia; if (selectedFilter !== 'all') { - // Check both source_service and the sources array for cross-service items filtered = allMedia.filter((m) => m.source_service === selectedFilter || (m.sources && m.sources.includes(selectedFilter)) @@ -328,7 +300,6 @@ const HomeScreen = ({ navigation, route }) => { ); } - // Build the list of available source filters const sourceFilters = [ { key: 'DEVICE', icon: '📱', name: 'On Device' }, ...linkedAccounts.map((acc) => ({ @@ -372,7 +343,6 @@ const HomeScreen = ({ navigation, route }) => { - {/* Import from Google Photos button */} {hasGoogleAccount && ( { - {/* Login button – blue */} { )} - {/* Register button – white/outlined */} navigation.navigate('Register')} diff --git a/src/screens/OnboardingScreen.js b/src/screens/OnboardingScreen.js index d34fde8..b482204 100644 --- a/src/screens/OnboardingScreen.js +++ b/src/screens/OnboardingScreen.js @@ -16,29 +16,24 @@ const OnboardingScreen = ({ navigation, route }) => { const [currentStep, setCurrentStep] = useState(0); const [loading, setLoading] = useState(false); - // Service toggles const [googleEnabled, setGoogleEnabled] = useState(false); const [iosEnabled, setIosEnabled] = useState(false); const [nasEnabled, setNasEnabled] = useState(false); - // Service connection status const [googleConnected, setGoogleConnected] = useState(false); const [iosConnected, setIosConnected] = useState(false); const [nasConnected, setNasConnected] = useState(false); - // Service health: 'inactive' | 'healthy' | 'error' | 'checking' const [googleHealth, setGoogleHealth] = useState('inactive'); const [iosHealth, setIosHealth] = useState('inactive'); const [nasHealth, setNasHealth] = useState('inactive'); - // NAS config const [nasHost, setNasHost] = useState(''); const [nasPort, setNasPort] = useState('22'); const [nasUsername, setNasUsername] = useState(''); const [nasPassword, setNasPassword] = useState(''); const [nasPhotoFolder, setNasPhotoFolder] = useState('/home/user/photos'); - // Auto-backup checklist (multi-select) const [autoBackupServices, setAutoBackupServices] = useState([]); const steps = [ @@ -165,7 +160,6 @@ const OnboardingScreen = ({ navigation, route }) => { return ( - {/* Progress bar */} {steps.map((_, i) => ( @@ -179,7 +173,6 @@ const OnboardingScreen = ({ navigation, route }) => { {steps[currentStep].description} - {/* Step 0: Select services */} {currentStep === 0 && ( { description="Connect to your Ubuntu server via SFTP" enabled={nasEnabled} onToggle={() => setNasEnabled(!nasEnabled)} /> - {/* NAS setup instructions dropdown */} {nasEnabled && } )} - {/* Step 1: Configure each service */} {currentStep === 1 && ( {googleEnabled && ( @@ -281,7 +272,6 @@ const OnboardingScreen = ({ navigation, route }) => { )} - {/* Step 2: Auto-backup multi-checkbox */} {currentStep === 2 && ( @@ -327,7 +317,6 @@ const OnboardingScreen = ({ navigation, route }) => { )} - {/* Footer */} {currentStep > 0 ? ( { /> - {/* Create Account button – blue */} { )} - {/* Back to Login button – white/outlined */} navigation.navigate('Login')} diff --git a/src/screens/SettingsScreen.js b/src/screens/SettingsScreen.js index 9d84baa..17ce7b7 100644 --- a/src/screens/SettingsScreen.js +++ b/src/screens/SettingsScreen.js @@ -18,11 +18,7 @@ const SettingsScreen = ({ navigation, route }) => { const [accounts, setAccounts] = useState([]); const [autoBackup, setAutoBackup] = useState([]); const [nasOffload, setNasOffload] = useState(false); - - // Per-service health: { GOOGLE_PHOTOS: 'healthy'|'error'|'inactive', ... } const [serviceHealth, setServiceHealth] = useState({}); - - // NAS edit fields const [editingNas, setEditingNas] = useState(false); const [nasHost, setNasHost] = useState(''); const [nasPort, setNasPort] = useState('22'); @@ -51,14 +47,12 @@ const SettingsScreen = ({ navigation, route }) => { setNasPhotoFolder(nasAcc.account_data.photoFolder || '/home/user/photos'); } - // Check health for each enabled account checkAllServiceHealth(allAccounts); } catch (err) { console.error('Error loading settings:', err); } }; - // ─── Health checks ───────────────────────────────────────── const checkAllServiceHealth = async (accs) => { const health = {}; for (const acc of accs) { @@ -76,8 +70,6 @@ const SettingsScreen = ({ navigation, route }) => { const checkSingleHealth = async (acc) => { try { if (acc.service_type === 'GOOGLE_PHOTOS') { - // The Photos Library REST API is deprecated; health-check the token - // by calling the userinfo endpoint instead. const gAuth = new GoogleAUTH(); gAuth.accessToken = acc.account_data?.accessToken; gAuth.refreshToken = acc.account_data?.refreshToken; @@ -93,7 +85,6 @@ const SettingsScreen = ({ navigation, route }) => { return 'healthy'; } if (acc.service_type === 'ICLOUD_PHOTOS') { - // Placeholder – always "healthy" since it's a stub return 'healthy'; } return 'inactive'; @@ -108,7 +99,6 @@ const SettingsScreen = ({ navigation, route }) => { setServiceHealth((prev) => ({ ...prev, [acc.service_type]: status })); }; - // ─── Service actions ─────────────────────────────────────── const toggleService = async (accountId, currentEnabled) => { try { await DatabaseService.toggleAccountEnabled(accountId, !currentEnabled); @@ -168,7 +158,7 @@ const SettingsScreen = ({ navigation, route }) => { } else if (acc.service_type === 'NAS') { setEditingNas(true); setLoading(false); - return; // user edits inline + return; } await loadData(); } catch (error) { @@ -249,7 +239,6 @@ const SettingsScreen = ({ navigation, route }) => { - {/* ── Theme ──────────────────────────────────────── */} APPEARANCE {themeOptions.map((opt) => ( @@ -272,7 +261,6 @@ const SettingsScreen = ({ navigation, route }) => { ))} - {/* ── Services ──────────────────────────────────── */} SERVICES {accounts.length === 0 ? ( @@ -286,7 +274,6 @@ const SettingsScreen = ({ navigation, route }) => { const health = serviceHealth[acc.service_type] || (acc.is_enabled ? 'checking' : 'inactive'); return ( - {/* Main row */} recheckHealth(acc)} activeOpacity={0.6}> @@ -307,7 +294,6 @@ const SettingsScreen = ({ navigation, route }) => { thumbColor={acc.is_enabled ? theme.accent : '#f4f3f4'} /> - {/* Action buttons row */} {acc.service_type === 'NAS' && acc.is_enabled && ( { )} - {/* NAS Edit Config */} {editingNas && ( Edit NAS Configuration @@ -364,10 +349,8 @@ const SettingsScreen = ({ navigation, route }) => { )} - {/* NAS Setup Instructions */} - {/* NAS Google Photos Offloading */} {hasNasConnected && ( <> NAS OFFLOADING @@ -391,7 +374,6 @@ const SettingsScreen = ({ navigation, route }) => { )} - {/* ── Auto Backup ───────────────────────────────── */} AUTOMATIC BACKUP @@ -452,7 +434,6 @@ const SettingsScreen = ({ navigation, route }) => { - {/* ── Sign Out ──────────────────────────────────── */} { if (initPromise) return initPromise; @@ -32,7 +24,6 @@ const DatabaseService = { return false; } - // Create all tables in one batch await db.execAsync(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -68,7 +59,6 @@ const DatabaseService = { ); `); - // Safe migrations — errors ignored if columns already exist try { await db.execAsync("ALTER TABLE users ADD COLUMN auto_backup_services TEXT DEFAULT '[]'"); } catch (_) {} try { await db.execAsync("ALTER TABLE users ADD COLUMN theme_preference TEXT DEFAULT 'light'"); } catch (_) {} @@ -94,8 +84,6 @@ const DatabaseService = { return true; }, - // ─── Users ───────────────────────────────────────────────── - createUser: async (username, email, password) => { try { if (!(await DatabaseService.ensureDb())) return null; @@ -155,8 +143,6 @@ const DatabaseService = { } }, - // ─── Linked Accounts ────────────────────────────────────── - linkAccount: async (userId, serviceType, accountData, isDefault = false) => { try { if (!(await DatabaseService.ensureDb())) return null; @@ -171,7 +157,6 @@ const DatabaseService = { return null; } - // Replace any existing link for the same service type await db.runAsync( 'DELETE FROM linked_accounts WHERE user_id = ? AND service_type = ?', [userId, serviceType] @@ -286,8 +271,6 @@ const DatabaseService = { } }, - // ─── Media ───────────────────────────────────────────────── - addMedia: async (userId, sourceService, sourceId, title, description, localPath, remotePath, mediaType, createdDate) => { try { if (!(await DatabaseService.ensureDb())) return null; @@ -308,16 +291,12 @@ const DatabaseService = { } }, - /** - * Save a normalised media item object to the DB. - * Skips duplicates (same user_id + source_id). - */ saveMediaItem: async (userId, item) => { try { if (!(await DatabaseService.ensureDb())) return null; - if (!userId || !item?.source_service) return null; + if (!userId || !item?.source_service) + return null; - // Skip if already exists if (item.source_id) { const existing = await db.getFirstAsync( 'SELECT id FROM media WHERE user_id = ? AND source_id = ?', @@ -384,8 +363,6 @@ const DatabaseService = { } }, - // ─── Settings ────────────────────────────────────────────── - getAutoBackupServices: async (userId) => { try { if (!(await DatabaseService.ensureDb())) return []; @@ -413,8 +390,6 @@ const DatabaseService = { } }, - // ─── Lifecycle ───────────────────────────────────────────── - closeDatabase: async () => { try { if (db?.closeAsync) await db.closeAsync(); diff --git a/src/services/DeviceMediaService.js b/src/services/DeviceMediaService.js index d471477..ab03ea2 100644 --- a/src/services/DeviceMediaService.js +++ b/src/services/DeviceMediaService.js @@ -1,18 +1,6 @@ import * as MediaLibrary from 'expo-media-library'; -/** - * Fetches photos and videos directly from the Android device media store. - * - * This replaces the deprecated Google Photos Library REST API. - * On Android, the media store includes all photos visible to the device — - * camera roll, screenshots, downloads, and images synced by the Google - * Photos app. No OAuth token is needed; only the MEDIA_LIBRARY permission. - */ const DeviceMediaService = { - /** - * Request media-library permission (camera roll / storage). - * Returns true if granted. - */ requestPermission: async () => { const { status } = await MediaLibrary.requestPermissionsAsync(); if (status !== 'granted') { @@ -22,14 +10,6 @@ const DeviceMediaService = { return true; }, - /** - * Fetch a page of media assets from the device. - * - * @param {number} pageSize Number of items per page (default 100). - * @param {string} after Cursor returned by a previous call (for pagination). - * @param {string} mediaType 'photo' | 'video' | 'all' (default 'all'). - * @returns {{ mediaItems: Array, endCursor: string|null, hasNextPage: boolean }} - */ getPhotos: async (pageSize = 100, after = null, mediaType = 'all') => { const hasPermission = await DeviceMediaService.requestPermission(); if (!hasPermission) { @@ -44,7 +24,7 @@ const DeviceMediaService = { const options = { first: pageSize, mediaType: mediaTypeFilter, - sortBy: [[MediaLibrary.SortBy.creationTime, false]], // newest first + sortBy: [[MediaLibrary.SortBy.creationTime, false]], }; if (after) options.after = after; @@ -73,9 +53,6 @@ const DeviceMediaService = { }; }, - /** - * Fetch all albums from the device (including smart albums). - */ getAlbums: async () => { const hasPermission = await DeviceMediaService.requestPermission(); if (!hasPermission) return []; @@ -91,9 +68,6 @@ const DeviceMediaService = { })); }, - /** - * Fetch assets inside a specific album. - */ getAlbumPhotos: async (albumId, pageSize = 100, after = null) => { const hasPermission = await DeviceMediaService.requestPermission(); if (!hasPermission) { diff --git a/src/services/ErrorHandler.js b/src/services/ErrorHandler.js index 81fb917..b234bc3 100644 --- a/src/services/ErrorHandler.js +++ b/src/services/ErrorHandler.js @@ -1,31 +1,10 @@ import { Alert } from 'react-native'; -/** - * Centralised error handler for the app. - * - * Usage: - * import { logError, alertError } from '../services/ErrorHandler'; - * - * logError('Google Auth', error); // console.error only - * alertError('Upload Failed', error); // Alert + console.error - */ - -/** - * Log an error to the console with a contextual tag. - * @param {string} tag – short label, e.g. 'Google Auth', 'NAS' - * @param {Error|string} error - */ export const logError = (tag, error) => { const message = error instanceof Error ? error.message : String(error); console.error(`[${tag}] ${message}`); }; -/** - * Show a user-facing Alert AND log to the console. - * @param {string} title – Alert title - * @param {Error|string} error - * @param {Array} [buttons] – optional Alert button array - */ export const alertError = (title, error, buttons) => { const message = error instanceof Error ? error.message : String(error); console.error(`[${title}] ${message}`); diff --git a/src/services/ThemeContext.js b/src/services/ThemeContext.js index 9355137..53e7459 100644 --- a/src/services/ThemeContext.js +++ b/src/services/ThemeContext.js @@ -2,8 +2,6 @@ import React, { createContext, useContext, useState, useEffect } from 'react'; import * as SecureStore from 'expo-secure-store'; const THEME_KEY = 'app_theme_preference'; - -// Available theme definitions const lightTheme = { name: 'light', background: '#ffffff',