mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-04-21 16:24:41 +01:00
feat: better logs
- added ability to write debug logs for development builds - added filtering to log page - modified filter button to allow for multiple selection if required
This commit is contained in:
@@ -1,37 +1,112 @@
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { useLog } from "@/utils/log";
|
||||
import {LogLevel, useLog} from "@/utils/log";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ScrollView, View } from "react-native";
|
||||
import {ScrollView, TouchableOpacity, View} from "react-native";
|
||||
import Collapsible from "react-native-collapsible";
|
||||
import React, {useMemo, useState} from "react";
|
||||
import {FilterButton} from "@/components/filters/FilterButton";
|
||||
|
||||
export default function page() {
|
||||
const { logs } = useLog();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const defaultLevels: LogLevel[] = ["INFO", "ERROR", "DEBUG", "WARN"]
|
||||
const codeBlockStyle = {
|
||||
backgroundColor: '#000',
|
||||
padding: 10,
|
||||
fontFamily: 'monospace',
|
||||
maxHeight: 300
|
||||
}
|
||||
|
||||
const [state, setState] = useState<Record<string, boolean>>({})
|
||||
|
||||
const [order, setOrder] = useState<"asc" | "desc">("desc");
|
||||
const [levels, setLevels] = useState<LogLevel[]>(defaultLevels);
|
||||
|
||||
const filteredLogs = useMemo(
|
||||
() => logs
|
||||
?.filter(log => levels.includes(log.level))
|
||||
// Already in asc order as they are recorded. just reverse for desc
|
||||
?.[order === "desc" ? "reverse" : "concat"]?.(),
|
||||
[logs, order, levels]
|
||||
)
|
||||
|
||||
return (
|
||||
<ScrollView className='p-4'>
|
||||
<View className='flex flex-col space-y-2'>
|
||||
{logs?.map((log, index) => (
|
||||
<View key={index} className='bg-neutral-900 rounded-xl p-3'>
|
||||
<Text
|
||||
className={`
|
||||
mb-1
|
||||
${log.level === "INFO" && "text-blue-500"}
|
||||
${log.level === "ERROR" && "text-red-500"}
|
||||
`}
|
||||
>
|
||||
{log.level}
|
||||
</Text>
|
||||
<Text uiTextView selectable className='text-xs'>
|
||||
{log.message}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
{logs?.length === 0 && (
|
||||
<Text className='opacity-50'>
|
||||
{t("home.settings.logs.no_logs_available")}
|
||||
</Text>
|
||||
)}
|
||||
<>
|
||||
<View className="flex flex-row justify-end py-2 px-4 space-x-2">
|
||||
<FilterButton
|
||||
id='order'
|
||||
queryKey='log'
|
||||
queryFn={async () => ["asc", "desc"]}
|
||||
set={(values) => setOrder(values[0])}
|
||||
values={[order]}
|
||||
title={t("library.filters.sort_order")}
|
||||
renderItemLabel={(order) => t(`library.filters.${order}`)}
|
||||
showSearch={false}
|
||||
/>
|
||||
<FilterButton
|
||||
id='levels'
|
||||
queryKey='log'
|
||||
queryFn={async () => defaultLevels}
|
||||
set={setLevels}
|
||||
values={levels}
|
||||
title={t("home.settings.logs.level")}
|
||||
renderItemLabel={(level) => level}
|
||||
showSearch={false}
|
||||
multiple={true}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
<ScrollView className='pb-4 px-4'>
|
||||
<View className='flex flex-col space-y-2'>
|
||||
{filteredLogs?.map((log, index) => (
|
||||
<View
|
||||
className='bg-neutral-900 rounded-xl p-3'
|
||||
key={index}
|
||||
>
|
||||
<TouchableOpacity
|
||||
disabled={!log.data}
|
||||
onPress={() => setState((v) => ({...v, [log.timestamp]: !v[log.timestamp]}))}
|
||||
>
|
||||
<View className="flex flex-row justify-between">
|
||||
<Text
|
||||
className={`mb-1
|
||||
${log.level === "INFO" && "text-blue-500"}
|
||||
${log.level === "ERROR" && "text-red-500"}
|
||||
${log.level === "DEBUG" && "text-purple-500"}
|
||||
`}>
|
||||
{log.level}
|
||||
</Text>
|
||||
|
||||
<Text className="text-xs">{new Date(log.timestamp).toLocaleString()}</Text>
|
||||
</View>
|
||||
<Text uiTextView selectable className='text-xs'>
|
||||
{log.message}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{log.data && (
|
||||
<>
|
||||
<Text className="text-xs">{t("home.settings.logs.click_for_more_info")}</Text>
|
||||
<Collapsible collapsed={!state[log.timestamp]}>
|
||||
<View className="mt-2 flex flex-col space-y-2">
|
||||
<ScrollView className="rounded-xl" style={codeBlockStyle}>
|
||||
<Text>
|
||||
{JSON.stringify(log.data, null, 2)}
|
||||
</Text>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</Collapsible>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
{filteredLogs?.length === 0 && (
|
||||
<Text className='opacity-50'>
|
||||
{t("home.settings.logs.no_logs_available")}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@ const page: React.FC = () => {
|
||||
component: (
|
||||
<FilterButton
|
||||
className='mr-1'
|
||||
collectionId={collectionId}
|
||||
id={collectionId}
|
||||
queryKey='genreFilter'
|
||||
queryFn={async () => {
|
||||
if (!api) return null;
|
||||
@@ -261,7 +261,7 @@ const page: React.FC = () => {
|
||||
component: (
|
||||
<FilterButton
|
||||
className='mr-1'
|
||||
collectionId={collectionId}
|
||||
id={collectionId}
|
||||
queryKey='yearFilter'
|
||||
queryFn={async () => {
|
||||
if (!api) return null;
|
||||
@@ -286,7 +286,7 @@ const page: React.FC = () => {
|
||||
component: (
|
||||
<FilterButton
|
||||
className='mr-1'
|
||||
collectionId={collectionId}
|
||||
id={collectionId}
|
||||
queryKey='tagsFilter'
|
||||
queryFn={async () => {
|
||||
if (!api) return null;
|
||||
@@ -313,7 +313,7 @@ const page: React.FC = () => {
|
||||
component: (
|
||||
<FilterButton
|
||||
className='mr-1'
|
||||
collectionId={collectionId}
|
||||
id={collectionId}
|
||||
queryKey='sortBy'
|
||||
queryFn={async () => sortOptions.map((s) => s.key)}
|
||||
set={setSortBy}
|
||||
@@ -333,7 +333,7 @@ const page: React.FC = () => {
|
||||
component: (
|
||||
<FilterButton
|
||||
className='mr-1'
|
||||
collectionId={collectionId}
|
||||
id={collectionId}
|
||||
queryKey='sortOrder'
|
||||
queryFn={async () => sortOrderOptions.map((s) => s.key)}
|
||||
set={setSortOrder}
|
||||
|
||||
@@ -287,7 +287,7 @@ const Page = () => {
|
||||
component: (
|
||||
<FilterButton
|
||||
className='mr-1'
|
||||
collectionId={libraryId}
|
||||
id={libraryId}
|
||||
queryKey='genreFilter'
|
||||
queryFn={async () => {
|
||||
if (!api) return null;
|
||||
@@ -314,7 +314,7 @@ const Page = () => {
|
||||
component: (
|
||||
<FilterButton
|
||||
className='mr-1'
|
||||
collectionId={libraryId}
|
||||
id={libraryId}
|
||||
queryKey='yearFilter'
|
||||
queryFn={async () => {
|
||||
if (!api) return null;
|
||||
@@ -339,7 +339,7 @@ const Page = () => {
|
||||
component: (
|
||||
<FilterButton
|
||||
className='mr-1'
|
||||
collectionId={libraryId}
|
||||
id={libraryId}
|
||||
queryKey='tagsFilter'
|
||||
queryFn={async () => {
|
||||
if (!api) return null;
|
||||
@@ -366,7 +366,7 @@ const Page = () => {
|
||||
component: (
|
||||
<FilterButton
|
||||
className='mr-1'
|
||||
collectionId={libraryId}
|
||||
id={libraryId}
|
||||
queryKey='sortBy'
|
||||
queryFn={async () => sortOptions.map((s) => s.key)}
|
||||
set={setSortBy}
|
||||
@@ -386,7 +386,7 @@ const Page = () => {
|
||||
component: (
|
||||
<FilterButton
|
||||
className='mr-1'
|
||||
collectionId={libraryId}
|
||||
id={libraryId}
|
||||
queryKey='sortOrder'
|
||||
queryFn={async () => sortOrderOptions.map((s) => s.key)}
|
||||
set={setSortOrder}
|
||||
|
||||
@@ -283,7 +283,7 @@ export default function search() {
|
||||
debouncedSearch.length > 0 && (
|
||||
<View className='flex flex-row justify-end items-center space-x-1'>
|
||||
<FilterButton
|
||||
collectionId='search'
|
||||
id='search'
|
||||
queryKey='jellyseerr_search'
|
||||
queryFn={async () =>
|
||||
Object.keys(JellyseerrSearchSort).filter((v) =>
|
||||
@@ -299,7 +299,7 @@ export default function search() {
|
||||
showSearch={false}
|
||||
/>
|
||||
<FilterButton
|
||||
collectionId='order'
|
||||
id='order'
|
||||
queryKey='jellysearr_search'
|
||||
queryFn={async () => ["asc", "desc"]}
|
||||
set={(value) => setJellyseerrSortOrder(value[0])}
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
BACKGROUND_FETCH_TASK_SESSIONS,
|
||||
registerBackgroundFetchAsyncSessions,
|
||||
} from "@/utils/background-tasks";
|
||||
import { LogProvider, writeErrorLog, writeToLog } from "@/utils/log";
|
||||
import {LogProvider, writeDebugLog, writeErrorLog, writeToLog} from "@/utils/log";
|
||||
import { storage } from "@/utils/mmkv";
|
||||
import { cancelJobById, getAllJobsByDeviceId } from "@/utils/optimize-server";
|
||||
import { ActionSheetProvider } from "@expo/react-native-action-sheet";
|
||||
@@ -357,21 +357,21 @@ function Layout() {
|
||||
Notifications?.addNotificationResponseReceivedListener(
|
||||
(response: NotificationResponse) => {
|
||||
// Currently the notifications supported by the plugin will send data for deep links.
|
||||
const data = response.notification.request.content.data;
|
||||
const {title, data} = response.notification.request.content;
|
||||
|
||||
writeDebugLog(`Notification ${title} opened`, response.notification.request.content)
|
||||
|
||||
if (data && Object.keys(data).length > 0) {
|
||||
const type = data["type"].toLower();
|
||||
const itemId = data["id"];
|
||||
const type = data?.type?.toLower?.();
|
||||
const itemId = data?.id;
|
||||
|
||||
switch (type) {
|
||||
case "movie":
|
||||
router.push(`/(auth)/(tabs)/home/items/page?id=${itemId}`)
|
||||
break;
|
||||
case "episode":
|
||||
const episodeId = data.id;
|
||||
|
||||
// We just clicked a notification for an individual episode.
|
||||
if (episodeId) {
|
||||
if (itemId) {
|
||||
router.push(`/(auth)/(tabs)/home/items/page?id=${itemId}`)
|
||||
}
|
||||
// summarized season notification for multiple episodes. Bring them to series season
|
||||
|
||||
Reference in New Issue
Block a user