Files
notesnook/apps/mobile/app/screens/settings/debug.tsx
2025-10-23 21:39:20 +05:00

337 lines
9.9 KiB
TypeScript

/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { sanitizeFilename } from "@notesnook/common";
import { format, LogLevel, logManager } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { LogMessage } from "@notesnook/logger";
import { useThemeColors } from "@notesnook/theme";
import Clipboard from "@react-native-clipboard/clipboard";
import React, { useEffect, useRef, useState } from "react";
import { FlatList, Platform, TouchableOpacity, View } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import * as ScopedStorage from "react-native-scoped-storage";
import filesystem from "../../common/filesystem";
import { presentDialog } from "../../components/dialog/functions";
import { IconButton } from "../../components/ui/icon-button";
import { Notice } from "../../components/ui/notice";
import Paragraph from "../../components/ui/typography/paragraph";
import useTimer from "../../hooks/use-timer";
import { ToastManager } from "../../services/event-manager";
import { hexToRGBA } from "../../utils/colors";
import { DefaultAppStyles } from "../../utils/styles";
export default function DebugLogs() {
const { colors } = useThemeColors();
const { seconds, start } = useTimer("debug_logs_timer");
const listRef = useRef<FlatList>(null);
const currentOffset = useRef(0);
const [logs, setLogs] = useState<
{
key: string;
logs: LogMessage[];
}[]
>([]);
const [currentLog, setCurrentLog] = useState<{
key: string;
logs: LogMessage[];
}>();
useEffect(() => {
(async () => {
if (seconds === 0) {
start(5, "debug_logs_timer");
const logs = await logManager?.get();
if (!logs) return;
if (logs.length > 0 && !currentLog) {
setCurrentLog(logs[0]);
} else {
setCurrentLog(logs[logs.findIndex((l) => l.key === currentLog?.key)]);
}
setLogs(logs);
}
})();
}, [currentLog, seconds, start]);
const renderItem = React.useCallback(
({ item }: { item: LogMessage; index: number }) => {
const background =
item.level === LogLevel.Error || item.level === LogLevel.Fatal
? hexToRGBA(colors.error.paragraph, 0.2)
: item.level === LogLevel.Warn
? hexToRGBA(colors.static.orange, 0.2)
: "transparent";
const color =
item.level === LogLevel.Error || item.level === LogLevel.Fatal
? colors.error.paragraph
: item.level === LogLevel.Warn
? colors.static.black
: colors.primary.paragraph;
return !item ? null : (
<TouchableOpacity
activeOpacity={1}
onLongPress={() => {
Clipboard.setString(format(item));
ToastManager.show({
heading: strings.logsCopied(),
context: "global",
type: "success"
});
}}
style={{
paddingHorizontal: DefaultAppStyles.GAP,
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
backgroundColor: background,
flexShrink: 1,
borderBottomWidth: 1,
borderBottomColor: colors.primary.border
}}
>
<Paragraph
style={{
flexShrink: 1,
flexWrap: "wrap",
fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace"
}}
size={12}
color={color}
>
{format(item)}
</Paragraph>
</TouchableOpacity>
);
},
[
colors.primary.paragraph,
colors.error.paragraph,
colors.static.black,
colors.static.orange,
colors.primary.border
]
);
const downloadLogs = React.useCallback(async () => {
try {
let path = null;
const fileName = sanitizeFilename(`notesnook_logs_${Date.now()}`);
const data = currentLog?.logs
.map((log) => {
return !log ? "" : format(log);
})
.join("\n");
if (!data) return;
if (Platform.OS === "android") {
const file = await ScopedStorage.createDocument(
fileName + ".txt",
"text/plain",
data,
"utf8"
);
if (!file) return;
path = file.uri;
} else {
path = await filesystem.checkAndCreateDir("/");
await RNFetchBlob.fs.writeFile(path + fileName + ".txt", data, "utf8");
path = path + fileName;
}
if (path) {
ToastManager.show({
heading: strings.logsDownloaded(),
context: "global",
type: "success"
});
}
} catch (e) {}
}, [currentLog?.logs]);
const copyLogs = React.useCallback(() => {
const data = currentLog?.logs
.map((log) => {
return !log ? "" : format(log);
})
.join("\n");
if (!data) return;
Clipboard.setString(data);
ToastManager.show({
heading: strings.logsCopied(),
context: "global",
type: "success"
});
}, [currentLog?.logs]);
const clearLogs = React.useCallback(() => {
if (!currentLog) return;
presentDialog({
title: strings.clearLogs(),
paragraph: strings.clearLogsConfirmation(currentLog.key),
negativeText: strings.cancel(),
positiveText: strings.clear(),
positivePress: async () => {
const index = logs.findIndex((l) => (l.key = currentLog.key));
logManager?.delete(currentLog.key);
if (logs.length > 1) {
if (logs.length - 1 === index) {
setCurrentLog(logs[index - 1]);
} else {
setCurrentLog(logs[index + 1]);
}
} else {
setCurrentLog(undefined);
}
}
});
}, [currentLog, logs]);
return (
<View
style={{
flex: 1
}}
>
<View
style={{
padding: DefaultAppStyles.GAP
}}
>
<Notice text={strings.debugNotice()} type="information" />
</View>
{currentLog && (
<FlatList
ref={listRef}
ListHeaderComponent={
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP,
marginBottom: DefaultAppStyles.GAP_VERTICAL,
flexDirection: "row",
alignItems: "center",
backgroundColor: colors.primary.background,
justifyContent: "space-between"
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center"
}}
>
<Paragraph>{currentLog.key}</Paragraph>
<IconButton
style={{
width: 30,
height: 30,
marginHorizontal: 5
}}
onPress={() => {
const index = logs.findIndex(
(l) => l.key === currentLog.key
);
if (index === 0) return;
setCurrentLog(logs[index - 1]);
}}
size={20}
name="chevron-left"
color={colors.primary.icon}
/>
<IconButton
style={{
width: 30,
height: 30
}}
onPress={() => {
const index = logs.findIndex(
(l) => l.key === currentLog.key
);
if (index === logs.length - 1) return;
setCurrentLog(logs[index + 1]);
}}
size={20}
name="chevron-right"
color={colors.primary.icon}
/>
</View>
<View
style={{
flexDirection: "row"
}}
>
<IconButton
onPress={copyLogs}
size={20}
style={{
width: 30,
height: 30,
marginRight: 5
}}
name="content-copy"
color={colors.secondary.paragraph}
/>
<IconButton
onPress={downloadLogs}
style={{
width: 30,
height: 30,
marginRight: 5
}}
size={20}
name="download"
color={colors.secondary.paragraph}
/>
<IconButton
onPress={clearLogs}
style={{
width: 30,
height: 30,
marginRight: 5
}}
size={20}
name="delete"
color={colors.secondary.paragraph}
/>
</View>
</View>
}
style={{
flex: 1,
width: "100%"
}}
stickyHeaderIndices={[0]}
ListFooterComponent={
<View
style={{
height: 200
}}
/>
}
data={currentLog.logs}
renderItem={renderItem}
/>
)}
</View>
);
}