1ac35c84e0
- Detail screen: Delete button (top-right, red) with confirmation alert; deletes SQLite row and original file via expo-file-system - Feed screen: long-press card to enter select mode; checkbox + blue border on selected cards; bottom action bar with bulk Delete N button; header switches to show count + Cancel - db/queries: deleteActivity (returns original_path) and deleteActivities (bulk, returns all original paths)
172 lines
5.4 KiB
TypeScript
172 lines
5.4 KiB
TypeScript
import { useSQLiteContext } from 'expo-sqlite';
|
|
|
|
// ── Types ──────────────────────────────────────────────────────────────────
|
|
|
|
export type ActivityRow = {
|
|
id: string;
|
|
source_hash: string;
|
|
detail_json: string;
|
|
timeseries_json: string | null;
|
|
geojson: string | null;
|
|
original_path: string | null;
|
|
synced_at: number | null;
|
|
origin: 'local' | 'remote';
|
|
created_at: number;
|
|
};
|
|
|
|
export type ActivitySummary = {
|
|
id: string;
|
|
title: string;
|
|
sport: string;
|
|
started_at: string;
|
|
distance_m: number | null;
|
|
duration_s: number | null;
|
|
elevation_gain_m: number | null;
|
|
origin: 'local' | 'remote';
|
|
synced_at: number | null;
|
|
};
|
|
|
|
// ── Activities ─────────────────────────────────────────────────────────────
|
|
|
|
export function useActivities(): ActivitySummary[] {
|
|
const db = useSQLiteContext();
|
|
// Summaries are derived from the stored detail_json at query time.
|
|
// JSON extraction via SQLite's json_extract keeps the table schema simple.
|
|
const rows = db.getAllSync<{
|
|
id: string;
|
|
origin: 'local' | 'remote';
|
|
synced_at: number | null;
|
|
title: string;
|
|
sport: string;
|
|
started_at: string;
|
|
distance_m: number | null;
|
|
duration_s: number | null;
|
|
elevation_gain_m: number | null;
|
|
}>(`
|
|
SELECT
|
|
id, origin, synced_at,
|
|
json_extract(detail_json, '$.title') AS title,
|
|
json_extract(detail_json, '$.sport') AS sport,
|
|
json_extract(detail_json, '$.started_at') AS started_at,
|
|
json_extract(detail_json, '$.distance_m') AS distance_m,
|
|
json_extract(detail_json, '$.duration_s') AS duration_s,
|
|
json_extract(detail_json, '$.elevation_gain_m') AS elevation_gain_m
|
|
FROM activities
|
|
ORDER BY json_extract(detail_json, '$.started_at') DESC
|
|
`);
|
|
return rows;
|
|
}
|
|
|
|
export function useActivity(id: string): ActivityRow | null {
|
|
const db = useSQLiteContext();
|
|
return db.getFirstSync<ActivityRow>(
|
|
'SELECT * FROM activities WHERE id = ?',
|
|
[id],
|
|
) ?? null;
|
|
}
|
|
|
|
export async function insertActivity(
|
|
db: ReturnType<typeof useSQLiteContext>,
|
|
row: Pick<ActivityRow, 'id' | 'source_hash' | 'detail_json' | 'timeseries_json' | 'geojson' | 'original_path' | 'origin'>,
|
|
): Promise<void> {
|
|
await db.runAsync(
|
|
`INSERT OR IGNORE INTO activities
|
|
(id, source_hash, detail_json, timeseries_json, geojson, original_path, origin)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
row.id,
|
|
row.source_hash,
|
|
row.detail_json,
|
|
row.timeseries_json ?? null,
|
|
row.geojson ?? null,
|
|
row.original_path ?? null,
|
|
row.origin,
|
|
],
|
|
);
|
|
}
|
|
|
|
export async function upsertRemoteActivity(
|
|
db: ReturnType<typeof useSQLiteContext>,
|
|
id: string,
|
|
detailJson: string,
|
|
): Promise<boolean> {
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const result = await db.runAsync(
|
|
`INSERT INTO activities (id, source_hash, detail_json, origin, synced_at)
|
|
VALUES (?, ?, ?, 'remote', ?)
|
|
ON CONFLICT(id) DO UPDATE SET
|
|
detail_json = excluded.detail_json,
|
|
synced_at = excluded.synced_at
|
|
WHERE origin = 'remote'`,
|
|
[id, id, detailJson, now],
|
|
);
|
|
return result.changes > 0;
|
|
}
|
|
|
|
export async function deleteRemoteActivities(
|
|
db: ReturnType<typeof useSQLiteContext>,
|
|
): Promise<number> {
|
|
const result = await db.runAsync(`DELETE FROM activities WHERE origin = 'remote'`);
|
|
return result.changes;
|
|
}
|
|
|
|
export async function deleteActivity(
|
|
db: ReturnType<typeof useSQLiteContext>,
|
|
id: string,
|
|
): Promise<string | null> {
|
|
const row = db.getFirstSync<{ original_path: string | null }>(
|
|
'SELECT original_path FROM activities WHERE id = ?',
|
|
[id],
|
|
);
|
|
await db.runAsync('DELETE FROM activities WHERE id = ?', [id]);
|
|
return row?.original_path ?? null;
|
|
}
|
|
|
|
export async function deleteActivities(
|
|
db: ReturnType<typeof useSQLiteContext>,
|
|
ids: string[],
|
|
): Promise<Array<string | null>> {
|
|
if (ids.length === 0) return [];
|
|
const rows = db.getAllSync<{ original_path: string | null }>(
|
|
`SELECT original_path FROM activities WHERE id IN (${ids.map(() => '?').join(',')})`,
|
|
ids,
|
|
);
|
|
const placeholders = ids.map(() => '?').join(',');
|
|
await db.runAsync(`DELETE FROM activities WHERE id IN (${placeholders})`, ids);
|
|
return rows.map(r => r.original_path ?? null);
|
|
}
|
|
|
|
// ── Settings ───────────────────────────────────────────────────────────────
|
|
|
|
export async function getSetting(
|
|
db: ReturnType<typeof useSQLiteContext>,
|
|
key: string,
|
|
): Promise<string | null> {
|
|
const row = db.getFirstSync<{ value: string }>(
|
|
'SELECT value FROM settings WHERE key = ?',
|
|
[key],
|
|
);
|
|
return row?.value ?? null;
|
|
}
|
|
|
|
export async function setSetting(
|
|
db: ReturnType<typeof useSQLiteContext>,
|
|
key: string,
|
|
value: string,
|
|
): Promise<void> {
|
|
await db.runAsync(
|
|
`INSERT INTO settings (key, value) VALUES (?, ?)
|
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
|
|
[key, value],
|
|
);
|
|
}
|
|
|
|
export function useSetting(key: string): string | null {
|
|
const db = useSQLiteContext();
|
|
const row = db.getFirstSync<{ value: string }>(
|
|
'SELECT value FROM settings WHERE key = ?',
|
|
[key],
|
|
);
|
|
return row?.value ?? null;
|
|
}
|