180 lines
5.6 KiB
TypeScript
180 lines
5.6 KiB
TypeScript
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
import { Plant, Language, AppearanceMode, ColorPalette } from '../types';
|
|
|
|
const STORAGE_KEY = 'greenlens_plants';
|
|
const LANG_KEY = 'greenlens_language';
|
|
const ONBOARDING_KEY = 'greenlens_onboarding_complete';
|
|
const APPEARANCE_MODE_KEY = 'greenlens_appearance_mode';
|
|
const COLOR_PALETTE_KEY = 'greenlens_color_palette';
|
|
const PROFILE_IMAGE_KEY = 'greenlens_profile_image';
|
|
const PROFILE_NAME_KEY = 'greenlens_profile_name';
|
|
const LEXICON_SEARCH_HISTORY_KEY = 'greenlens_lexicon_search_history';
|
|
const LEXICON_SEARCH_HISTORY_LIMIT = 10;
|
|
const DEFAULT_PROFILE_NAME = 'GreenLens User';
|
|
|
|
const isAppearanceMode = (value: string | null): value is AppearanceMode =>
|
|
value === 'system' || value === 'light' || value === 'dark';
|
|
|
|
const isColorPalette = (value: string | null): value is ColorPalette =>
|
|
value === 'forest' || value === 'ocean' || value === 'sunset' || value === 'mono';
|
|
|
|
const normalizeSearchQuery = (value: string): string => {
|
|
return value
|
|
.toLowerCase()
|
|
.normalize('NFD')
|
|
.replace(/[\u0300-\u036f]/g, '')
|
|
.trim()
|
|
.replace(/\s+/g, ' ');
|
|
};
|
|
|
|
export const StorageService = {
|
|
getPlants: async (): Promise<Plant[]> => {
|
|
try {
|
|
const json = await AsyncStorage.getItem(STORAGE_KEY);
|
|
return json ? JSON.parse(json) : [];
|
|
} catch (e) {
|
|
console.error('Failed to load plants', e);
|
|
return [];
|
|
}
|
|
},
|
|
|
|
savePlant: async (plant: Plant): Promise<void> => {
|
|
const plants = await StorageService.getPlants();
|
|
const updatedPlants = [plant, ...plants];
|
|
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(updatedPlants));
|
|
},
|
|
|
|
deletePlant: async (id: string): Promise<void> => {
|
|
const plants = await StorageService.getPlants();
|
|
const updatedPlants = plants.filter(p => p.id !== id);
|
|
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(updatedPlants));
|
|
},
|
|
|
|
updatePlant: async (updatedPlant: Plant): Promise<void> => {
|
|
const plants = await StorageService.getPlants();
|
|
const index = plants.findIndex(p => p.id === updatedPlant.id);
|
|
if (index !== -1) {
|
|
plants[index] = updatedPlant;
|
|
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(plants));
|
|
}
|
|
},
|
|
|
|
getLanguage: async (): Promise<Language> => {
|
|
try {
|
|
const lang = await AsyncStorage.getItem(LANG_KEY);
|
|
return (lang as Language) || 'en';
|
|
} catch (e) {
|
|
return 'en';
|
|
}
|
|
},
|
|
|
|
saveLanguage: async (lang: Language): Promise<void> => {
|
|
await AsyncStorage.setItem(LANG_KEY, lang);
|
|
},
|
|
|
|
getAppearanceMode: async (): Promise<AppearanceMode> => {
|
|
try {
|
|
const mode = await AsyncStorage.getItem(APPEARANCE_MODE_KEY);
|
|
return isAppearanceMode(mode) ? mode : 'system';
|
|
} catch (e) {
|
|
return 'system';
|
|
}
|
|
},
|
|
|
|
saveAppearanceMode: async (mode: AppearanceMode): Promise<void> => {
|
|
await AsyncStorage.setItem(APPEARANCE_MODE_KEY, mode);
|
|
},
|
|
|
|
getColorPalette: async (): Promise<ColorPalette> => {
|
|
try {
|
|
const palette = await AsyncStorage.getItem(COLOR_PALETTE_KEY);
|
|
return isColorPalette(palette) ? palette : 'forest';
|
|
} catch (e) {
|
|
return 'forest';
|
|
}
|
|
},
|
|
|
|
saveColorPalette: async (palette: ColorPalette): Promise<void> => {
|
|
await AsyncStorage.setItem(COLOR_PALETTE_KEY, palette);
|
|
},
|
|
|
|
getProfileImage: async (): Promise<string | null> => {
|
|
try {
|
|
const imageUri = await AsyncStorage.getItem(PROFILE_IMAGE_KEY);
|
|
return imageUri || null;
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
},
|
|
|
|
saveProfileImage: async (imageUri: string | null): Promise<void> => {
|
|
if (!imageUri) {
|
|
await AsyncStorage.removeItem(PROFILE_IMAGE_KEY);
|
|
return;
|
|
}
|
|
await AsyncStorage.setItem(PROFILE_IMAGE_KEY, imageUri);
|
|
},
|
|
|
|
getProfileName: async (): Promise<string> => {
|
|
try {
|
|
const profileName = await AsyncStorage.getItem(PROFILE_NAME_KEY);
|
|
const normalized = profileName?.trim();
|
|
return normalized || DEFAULT_PROFILE_NAME;
|
|
} catch (e) {
|
|
return DEFAULT_PROFILE_NAME;
|
|
}
|
|
},
|
|
|
|
saveProfileName: async (name: string): Promise<void> => {
|
|
const normalized = name.trim();
|
|
await AsyncStorage.setItem(PROFILE_NAME_KEY, normalized || DEFAULT_PROFILE_NAME);
|
|
},
|
|
|
|
getOnboardingComplete: async (): Promise<boolean> => {
|
|
try {
|
|
const value = await AsyncStorage.getItem(ONBOARDING_KEY);
|
|
return value === 'true';
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
},
|
|
|
|
setOnboardingComplete: async (complete: boolean): Promise<void> => {
|
|
await AsyncStorage.setItem(ONBOARDING_KEY, complete ? 'true' : 'false');
|
|
},
|
|
|
|
getLexiconSearchHistory: async (): Promise<string[]> => {
|
|
try {
|
|
const value = await AsyncStorage.getItem(LEXICON_SEARCH_HISTORY_KEY);
|
|
if (!value) return [];
|
|
|
|
const parsed = JSON.parse(value);
|
|
if (!Array.isArray(parsed)) return [];
|
|
|
|
return parsed.filter((item): item is string => typeof item === 'string');
|
|
} catch (e) {
|
|
console.error('Failed to load lexicon search history', e);
|
|
return [];
|
|
}
|
|
},
|
|
|
|
saveLexiconSearchQuery: async (query: string): Promise<void> => {
|
|
const trimmed = query.trim();
|
|
if (!trimmed) return;
|
|
|
|
const history = await StorageService.getLexiconSearchHistory();
|
|
const normalized = normalizeSearchQuery(trimmed);
|
|
|
|
const deduped = history.filter(
|
|
item => normalizeSearchQuery(item) !== normalized
|
|
);
|
|
|
|
const updated = [trimmed, ...deduped].slice(0, LEXICON_SEARCH_HISTORY_LIMIT);
|
|
await AsyncStorage.setItem(LEXICON_SEARCH_HISTORY_KEY, JSON.stringify(updated));
|
|
},
|
|
|
|
clearLexiconSearchHistory: async (): Promise<void> => {
|
|
await AsyncStorage.removeItem(LEXICON_SEARCH_HISTORY_KEY);
|
|
},
|
|
};
|