Massive/SettingsPage.tsx

413 lines
13 KiB
TypeScript
Raw Normal View History

import { NavigationProp, useNavigation } from "@react-navigation/native";
import { format } from "date-fns";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { NativeModules, ScrollView } from "react-native";
import DocumentPicker from "react-native-document-picker";
import { Dirs, FileSystem } from "react-native-file-access";
import { Button, TextInput } from "react-native-paper";
import ConfirmDialog from "./ConfirmDialog";
import DrawerHeader from "./DrawerHeader";
import Page from "./Page";
import Select from "./Select";
import Switch from "./Switch";
import { MARGIN } from "./constants";
import { AppDataSource } from "./data-source";
import { setRepo, settingsRepo } from "./db";
import { DrawerParams } from "./drawer-param-list";
import Input from "./input";
import { darkOptions, lightOptions, themeOptions } from "./options";
import Settings, { settingsUpdated } from "./settings";
import { toast } from "./toast";
import { useTheme } from "./use-theme";
import { check, PERMISSIONS, RESULTS, request } from "react-native-permissions";
import AppInput from "./AppInput";
2022-07-08 03:45:24 +00:00
const twelveHours = [
"dd/LL/yyyy",
"dd/LL/yyyy, p",
"ccc p",
"p",
"yyyy-MM-d",
"yyyy-MM-d, p",
"yyyy.MM.d",
];
const twentyFours = [
"dd/LL/yyyy",
"dd/LL/yyyy, k:m",
"ccc k:m",
"k:m",
"yyyy-MM-d",
"yyyy-MM-d, k:m",
"yyyy.MM.d",
];
2022-11-21 05:15:43 +00:00
export default function SettingsPage() {
const [ignoring, setIgnoring] = useState(false);
const [term, setTerm] = useState("");
const [formatOptions, setFormatOptions] = useState<string[]>(twelveHours);
const [importing, setImporting] = useState(false);
const [deleting, setDeleting] = useState(false);
const { reset } = useNavigation<NavigationProp<DrawerParams>>();
2023-06-27 03:16:59 +00:00
const { watch, setValue } = useForm<Settings>({
defaultValues: () => settingsRepo.findOne({ where: {} }),
});
const settings = watch();
2023-06-27 03:16:59 +00:00
const {
theme,
setTheme,
lightColor,
setLightColor,
darkColor,
setDarkColor,
} = useTheme();
useEffect(() => {
NativeModules.SettingsModule.ignoringBattery(setIgnoring);
NativeModules.SettingsModule.is24().then((is24: boolean) => {
console.log(`${SettingsPage.name}.focus:`, { is24 });
if (is24) setFormatOptions(twentyFours);
else setFormatOptions(twelveHours);
});
}, []);
2022-07-03 01:50:01 +00:00
const update = useCallback(async (key: keyof Settings, value: unknown) => {
await settingsRepo
2023-01-03 01:59:19 +00:00
.createQueryBuilder()
.update()
2023-06-27 03:16:59 +00:00
.set({ [key]: value })
2023-01-03 01:59:19 +00:00
.printSql()
.execute();
settingsUpdated();
}, []);
2023-01-03 01:59:19 +00:00
2022-12-28 01:15:02 +00:00
const soundString = useMemo(() => {
if (!settings.sound) return null;
const split = settings.sound.split("/");
return split.pop();
}, [settings.sound]);
2022-12-28 01:15:02 +00:00
const changeSound = useCallback(async () => {
2023-06-27 03:16:59 +00:00
const { fileCopyUri } = await DocumentPicker.pickSingle({
2023-03-27 01:01:37 +00:00
type: DocumentPicker.types.audio,
copyTo: "documentDirectory",
});
if (!fileCopyUri) return;
setValue("sound", fileCopyUri);
await update("sound", fileCopyUri);
toast("Sound will play after rest timers.");
}, [setValue, update]);
2023-01-01 00:32:26 +00:00
const switches: Input<boolean>[] = useMemo(
() => [
{ name: "Rest timers", value: settings.alarm, key: "alarm" },
{ name: "Vibrate", value: settings.vibrate, key: "vibrate" },
{ name: "Disable sound", value: settings.noSound, key: "noSound" },
{ name: "Notifications", value: settings.notify, key: "notify" },
{ name: "Show images", value: settings.images, key: "images" },
{ name: "Show unit", value: settings.showUnit, key: "showUnit" },
{ name: "Show steps", value: settings.steps, key: "steps" },
{ name: "Show date", value: settings.showDate, key: "showDate" },
{ name: "Automatic backup", value: settings.backup, key: "backup" },
2023-01-01 00:32:26 +00:00
],
[settings]
);
const filter = useCallback(
2023-06-27 03:16:59 +00:00
({ name }) => name.toLowerCase().includes(term.toLowerCase()),
[term]
);
const changeBoolean = useCallback(
async (key: keyof Settings, value: boolean) => {
setValue(key, value);
await update(key, value);
switch (key) {
case "alarm":
if (value) toast("Timers will now run after each set.");
else toast("Stopped timers running after each set.");
const canNotify = await check(PERMISSIONS.ANDROID.POST_NOTIFICATIONS);
if (canNotify === RESULTS.DENIED || canNotify === RESULTS.BLOCKED)
await request(PERMISSIONS.ANDROID.POST_NOTIFICATIONS);
if (value && !ignoring) NativeModules.SettingsModule.ignoreBattery();
return;
case "vibrate":
if (value) toast("Alarms will now vibrate.");
else toast("Alarms will no longer vibrate.");
return;
case "notify":
if (value) toast("Show notifications for new records.");
else toast("Stopped notifications for new records.");
return;
case "images":
if (value) toast("Show images for sets.");
else toast("Hid images for sets.");
return;
case "showUnit":
if (value) toast("Show option to select unit for sets.");
else toast("Hid unit option for sets.");
return;
case "steps":
if (value) toast("Show steps for exercises.");
else toast("Hid steps for exercises.");
return;
case "showDate":
if (value) toast("Show date for sets.");
else toast("Hid date on sets.");
return;
case "noSound":
if (value) toast("Alarms will no longer make a sound.");
else toast("Enabled sound for alarms.");
return;
case "backup":
if (value) {
const result = await DocumentPicker.pickDirectory();
toast("Backup database daily.");
NativeModules.BackupModule.start(result.uri);
} else {
toast("Stopped backing up daily");
NativeModules.BackupModule.stop();
}
return;
}
},
[ignoring, setValue, update]
);
2022-10-31 08:00:10 +00:00
2022-12-01 02:45:18 +00:00
const renderSwitch = useCallback(
(item: Input<boolean>) => (
<Switch
key={item.name}
value={item.value}
2023-06-27 03:16:59 +00:00
onChange={(value) => changeBoolean(item.key, value)}
title={item.name}
/>
),
[changeBoolean]
);
const switchesMarkup = useMemo(
2023-06-27 03:16:59 +00:00
() => switches.filter(filter).map((s) => renderSwitch(s)),
[filter, switches, renderSwitch]
);
const changeString = useCallback(
async (key: keyof Settings, value: string) => {
setValue(key, value);
await update(key, value);
switch (key) {
case "date":
return toast("Changed date format");
case "darkColor":
setDarkColor(value);
return toast("Set primary color for dark mode.");
case "lightColor":
setLightColor(value);
return toast("Set primary color for light mode.");
case "vibrate":
return toast("Set primary color for light mode.");
case "sound":
return toast("Sound will play after rest timers.");
case "theme":
setTheme(value as string);
if (value === "dark") toast("Theme will always be dark.");
else if (value === "light") toast("Theme will always be light.");
else if (value === "system") toast("Theme will follow system.");
return;
}
2022-12-01 02:45:18 +00:00
},
[update, setTheme, setDarkColor, setLightColor, setValue]
);
const changeNumber = useCallback(
async (key: keyof Settings, value: number) => {
setValue(key, value);
await update(key, value);
switch (key) {
case "duration":
return toast("Changed duration of alarm vibrations.");
}
},
[update, setValue]
);
const numberInputs: Input<number>[] = useMemo(
() => [
{
name: "Vibration duration (ms)",
value: settings.duration,
key: "duration",
},
],
[settings]
);
const renderNumber = useCallback(
(item: Input<number>) => (
<AppInput
value={item.value?.toString() ?? "300"}
key={item.key}
label={item.name}
onChangeText={(value) => changeString(item.key, value)}
onSubmitEditing={(e) =>
changeNumber(item.key, Number(e.nativeEvent.text))
}
keyboardType="numeric"
blurOnSubmit
/>
),
[changeString, changeNumber]
);
const numbersMarkup = useMemo(
() => numberInputs.filter(filter).map((s) => renderNumber(s)),
[numberInputs, filter, renderNumber]
);
const selects: Input<string>[] = useMemo(() => {
const today = new Date();
return [
{ name: "Theme", value: theme, items: themeOptions, key: "theme" },
{
name: "Dark color",
value: darkColor,
items: lightOptions,
key: "darkColor",
},
{
name: "Light color",
value: lightColor,
items: darkOptions,
key: "lightColor",
},
{
name: "Date format",
value: settings.date,
2023-06-27 03:16:59 +00:00
items: formatOptions.map((option) => ({
label: format(today, option),
value: option,
})),
key: "date",
},
];
}, [settings, darkColor, formatOptions, theme, lightColor]);
2022-12-01 02:45:18 +00:00
const renderSelect = useCallback(
2023-08-13 22:50:44 +00:00
(input: Input<string>) => (
2022-12-01 02:45:18 +00:00
<Select
2023-08-13 22:50:44 +00:00
key={input.name}
value={input.value}
onChange={(value) => changeString(input.key, value)}
label={input.name}
items={input.items}
2022-12-01 02:45:18 +00:00
/>
),
[changeString]
);
2022-12-01 02:45:18 +00:00
const selectsMarkup = useMemo(
() => selects.filter(filter).map(renderSelect),
[filter, selects, renderSelect]
);
2023-07-20 02:55:19 +00:00
const confirmDelete = useCallback(async () => {
setDeleting(false);
await AppDataSource.dropDatabase();
await AppDataSource.destroy();
await AppDataSource.initialize();
toast("Database deleted.");
}, []);
const confirmImport = useCallback(async () => {
setImporting(false);
await AppDataSource.destroy();
const file = await DocumentPicker.pickSingle();
await FileSystem.cp(file.uri, Dirs.DatabaseDir + "/massive.db");
await AppDataSource.initialize();
await setRepo.createQueryBuilder().update().set({ image: null }).execute();
await update("sound", null);
const { alarm, backup } = await settingsRepo.findOne({ where: {} });
console.log({ backup });
const directory = await DocumentPicker.pickDirectory();
if (backup) NativeModules.BackupModule.start(directory.uri);
else NativeModules.BackupModule.stop();
NativeModules.SettingsModule.ignoringBattery((isIgnoring: boolean) => {
if (alarm && !isIgnoring) NativeModules.SettingsModule.ignoreBattery();
reset({ index: 0, routes: [{ name: "Settings" }] });
});
}, [reset, update]);
const exportDatabase = useCallback(async () => {
const result = await check(PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE);
if (result === RESULTS.DENIED || result === RESULTS.BLOCKED)
await request(PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE);
const path = Dirs.DatabaseDir + "/massive.db";
await FileSystem.cpExternal(path, "massive.db", "downloads");
toast("Database exported. Check downloads.");
}, []);
const buttons = useMemo(
() => [
{
name: `Alarm sound: ${soundString || "Default"}`,
onPress: changeSound,
},
{ name: "Export database", onPress: exportDatabase },
{ name: "Import database", onPress: () => setImporting(true) },
{ name: "Delete database", onPress: () => setDeleting(true) },
],
[changeSound, exportDatabase, soundString]
);
const buttonsMarkup = useMemo(
() =>
buttons.filter(filter).map((button) => (
<Button
key={button.name}
style={{ alignSelf: "flex-start" }}
onPress={button.onPress}
>
{button.name}
</Button>
)),
[buttons, filter]
);
2022-12-24 00:36:11 +00:00
return (
<>
<DrawerHeader name="Settings" />
2022-12-24 00:36:11 +00:00
2023-06-27 03:16:59 +00:00
<Page term={term} search={setTerm} style={{ flexGrow: 1 }}>
<ScrollView style={{ marginTop: MARGIN, flex: 1 }}>
{selectsMarkup}
{numbersMarkup}
{switchesMarkup}
{buttonsMarkup}
</ScrollView>
</Page>
<ConfirmDialog
title="Are you sure?"
onOk={confirmImport}
setShow={setImporting}
2023-06-27 03:16:59 +00:00
show={importing}
>
Importing a database overwrites your current data. This action cannot be
reversed!
</ConfirmDialog>
<ConfirmDialog
title="Are you sure?"
onOk={confirmDelete}
setShow={setDeleting}
show={deleting}
>
Deleting your database wipes your current data. This action cannot be
reversed!
</ConfirmDialog>
</>
);
2022-07-03 01:50:01 +00:00
}