Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added automatic download / delete chapters #337

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
34 changes: 33 additions & 1 deletion src/components/general/DashboardPage.tsx
Original file line number Diff line number Diff line change
@@ -35,10 +35,19 @@ import {
autoBackupState,
chapterLanguagesState,
refreshOnStartState,
OnStartDownloadUnreadCountState,
OnStartUpDeleteReadState,
OnStartUpDownloadUnreadState,
customDownloadsDirState,
} from '../../state/settingStates';
import DashboardSidebarLink from './DashboardSidebarLink';
import { downloadCover } from '../../util/download';
import { createAutoBackup } from '../../util/backup';
import {
DeleteReadChapters,
DownloadUnreadChapters,
} from '../../features/library/chapterDownloadUtils';
import { getDefaultDownloadDir } from '../settings/GeneralSettings';

// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface Props {}
@@ -56,6 +65,11 @@ const DashboardPage: React.FC<Props> = () => {
const [importing, setImporting] = useRecoilState(importingState);
const categoryList = useRecoilValue(categoryListState);

const OnStartUpDownloadUnread = useRecoilValue(OnStartUpDownloadUnreadState);
const OnStartUpDownloadUnreadCount = useRecoilValue(OnStartDownloadUnreadCountState);
const OnStartUpDeleteRead = useRecoilValue(OnStartUpDeleteReadState);
const customDownloadsDir = useRecoilValue(customDownloadsDirState);

useEffect(() => {
if (autoBackup) {
createAutoBackup(autoBackupCount);
@@ -68,7 +82,25 @@ const DashboardPage: React.FC<Props> = () => {
setReloadingSeriesList,
chapterLanguages,
categoryList
).catch((e) => log.error(e));
)
.then(() => {
if (OnStartUpDeleteRead) {
DeleteReadChapters(
library.fetchSeriesList(),
customDownloadsDir || String(getDefaultDownloadDir())
);
}
if (OnStartUpDownloadUnread) {
DownloadUnreadChapters(
library.fetchSeriesList(),
customDownloadsDir || String(getDefaultDownloadDir()),
chapterLanguages,
false,
OnStartUpDownloadUnreadCount
);
}
})
.catch((e) => log.error(e));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeSeriesList]);
34 changes: 34 additions & 0 deletions src/components/library/SeriesDetails.tsx
Original file line number Diff line number Diff line change
@@ -29,11 +29,25 @@
import SeriesDetailsIntro from './series/SeriesDetailsIntro';
import SeriesDetailsInfoGrid from './series/SeriesDetailsInfoGrid';

import {
OnSeriesDetailsDeleteReadState,
OnSeriesDetailsDownloadUnreadState,
OnStartDownloadUnreadCountState,
chapterLanguagesState,
customDownloadsDirState,
} from '../../state/settingStates';
import {
DeleteReadChapters,
DownloadUnreadChapters,
} from '../../features/library/chapterDownloadUtils';
import { getDefaultDownloadDir } from '../settings/GeneralSettings';

type Props = unknown;

const SeriesDetails: React.FC<Props> = () => {
const { id } = useParams<{ id: string }>();
let series: Series = library.fetchSeries(id!)!;

Check warning on line 49 in src/components/library/SeriesDetails.tsx

GitHub Actions / build (18, macos-latest)

Forbidden non-null assertion

Check warning on line 49 in src/components/library/SeriesDetails.tsx

GitHub Actions / build (18, macos-latest)

Forbidden non-null assertion
const seriesArr: Series[] = new Array(1);

const location = useLocation();
const setExtensionMetadata = useSetRecoilState(currentExtensionMetadataState);
@@ -47,10 +61,17 @@
const setSeriesBannerUrl = useSetRecoilState(seriesBannerUrlState);
const setChapterFilterTitle = useSetRecoilState(chapterFilterTitleState);
const setChapterFilterGroup = useSetRecoilState(chapterFilterGroupState);

const customDownloadsDir = useRecoilValue(customDownloadsDirState);
const OnStartUpDownloadUnreadCount = useRecoilValue(OnStartDownloadUnreadCountState);
const OnSeriesDetailsDownloadUnread = useRecoilValue(OnSeriesDetailsDownloadUnreadState);
const OnSeriesDetailsDeleteRead = useRecoilValue(OnSeriesDetailsDeleteReadState);
const chapterLanguages = useRecoilValue(chapterLanguagesState);

const loadContent = async () => {
log.info(`Series page is loading details from database for series ${id}`);

series = library.fetchSeries(id!)!;

Check warning on line 74 in src/components/library/SeriesDetails.tsx

GitHub Actions / build (18, macos-latest)

Forbidden non-null assertion

Check warning on line 74 in src/components/library/SeriesDetails.tsx

GitHub Actions / build (18, macos-latest)

Forbidden non-null assertion
setSeries(series);
setChapterList(library.fetchChapters(id!));

@@ -70,6 +91,19 @@

useEffect(() => {
loadContent();
seriesArr[0] = series;
if (OnSeriesDetailsDeleteRead) {
DeleteReadChapters(seriesArr, customDownloadsDir || String(getDefaultDownloadDir()));
}
if (OnSeriesDetailsDownloadUnread) {
DownloadUnreadChapters(
seriesArr,
customDownloadsDir || String(getDefaultDownloadDir()),
chapterLanguages,
false,
OnStartUpDownloadUnreadCount
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, seriesList]);

20 changes: 20 additions & 0 deletions src/components/reader/ReaderPage.tsx
Original file line number Diff line number Diff line change
@@ -25,6 +25,8 @@ import { updateTitlebarText } from '../../util/titlebar';
import * as libraryStates from '../../state/libraryStates';
import * as readerStates from '../../state/readerStates';
import * as settingStates from '../../state/settingStates';
import { DownloadUnreadChapters } from '../../features/library/chapterDownloadUtils';
import { getDefaultDownloadDir } from '../settings/GeneralSettings';
import {
nextOffsetPages,
nextPageStyle,
@@ -99,6 +101,14 @@ const ReaderPage: React.FC<Props> = (props: Props) => {
const keyToggleFullscreen = useRecoilValue(settingStates.keyToggleFullscreenState);
const keyExit = useRecoilValue(settingStates.keyExitState);
const keyCloseOrBack = useRecoilValue(settingStates.keyCloseOrBackState);
const OnStartUpDownloadUnreadCount = useRecoilValue(
settingStates.OnStartDownloadUnreadCountState
);
const OnScrollingChaptersDownloadUnread = useRecoilValue(
settingStates.OnScrollingChaptersDownloadUnreadState
);

const seriesArr: Series[] = new Array(1);

/**
* Populate the relevantChapterList prop.
@@ -281,6 +291,16 @@ const ReaderPage: React.FC<Props> = (props: Props) => {
if (newChapterId === null) return false;
const desiredPage = fromPageMovement && previous ? Infinity : 1;
setChapter(newChapterId, desiredPage);
seriesArr[0] = library.fetchSeries(series_id!)!;
if (OnScrollingChaptersDownloadUnread) {
DownloadUnreadChapters(
seriesArr,
customDownloadsDir || String(getDefaultDownloadDir()),
chapterLanguages,
false,
OnStartUpDownloadUnreadCount
);
}
return true;
};

127 changes: 126 additions & 1 deletion src/components/settings/GeneralSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import { Language, LanguageKey, Languages } from '@tiyo/common';
import { ipcRenderer } from 'electron';
import { useRecoilState } from 'recoil';
@@ -13,6 +13,7 @@ import {
Stack,
Text,
Tooltip,
Accordion,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons';
import { GeneralSetting } from '../../models/types';
@@ -27,6 +28,12 @@ import {
customDownloadsDirState,
libraryCropCoversState,
refreshOnStartState,
OnScrollingChaptersDownloadUnreadState,
OnSeriesDetailsDeleteReadState,
OnSeriesDetailsDownloadUnreadState,
OnStartDownloadUnreadCountState,
OnStartUpDeleteReadState,
OnStartUpDownloadUnreadState,
} from '../../state/settingStates';

const languageOptions = Object.values(Languages)
@@ -48,6 +55,24 @@ const GeneralSettings: React.FC<Props> = () => {
const [libraryCropCovers, setLibraryCropCovers] = useRecoilState(libraryCropCoversState);
const [customDownloadsDir, setCustomDownloadsDir] = useRecoilState(customDownloadsDirState);

const [OnStartUpDownloadUnread, setOnStartUpDownloadUnread] = useRecoilState(
OnStartUpDownloadUnreadState
);
const [OnSeriesDetailsDownloadUnread, setOnSeriesDetailsDownloadUnread] = useRecoilState(
OnSeriesDetailsDownloadUnreadState
);
const [OnScrollingChaptersDownloadUnread, setOnScrollingChaptersDownloadUnread] = useRecoilState(
OnScrollingChaptersDownloadUnreadState
);
const [OnSeriesDetailsDeleteRead, setOnSeriesDetailsDeleteRead] = useRecoilState(
OnSeriesDetailsDeleteReadState
);
const [OnStartUpDeleteRead, setOnStartUpDeleteRead] = useRecoilState(OnStartUpDeleteReadState);
const [OnStartDownloadUnreadCount, setOnStartDownloadUnreadCount] = useRecoilState(
OnStartDownloadUnreadCountState
);
const [automation, setautomation] = useState<string[]>([]);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const updateGeneralSetting = (generalSetting: GeneralSetting, value: any) => {
switch (generalSetting) {
@@ -75,6 +100,24 @@ const GeneralSettings: React.FC<Props> = () => {
case GeneralSetting.autoBackupCount:
setAutoBackupCount(value);
break;
case GeneralSetting.OnStartUpDownloadUnread:
setOnStartUpDownloadUnread(value);
break;
case GeneralSetting.OnStartUpDeleteRead:
setOnStartUpDeleteRead(value);
break;
case GeneralSetting.OnStartUpDownloadUnreadCount:
setOnStartDownloadUnreadCount(value);
break;
case GeneralSetting.OnSeriesDetailsDownloadUnread:
setOnSeriesDetailsDownloadUnread(value);
break;
case GeneralSetting.OnSeriesDetailsDeleteRead:
setOnSeriesDetailsDeleteRead(value);
break;
case GeneralSetting.OnScrollingChaptersDownloadUnread:
setOnScrollingChaptersDownloadUnread(value);
break;
default:
break;
}
@@ -208,8 +251,90 @@ const GeneralSettings: React.FC<Props> = () => {
</Group>
</Flex>
</Stack>

<Text>Automation</Text>
<Accordion multiple value={automation} onChange={setautomation}>
<Accordion.Item value="download">
<Accordion.Control>Auto Download</Accordion.Control>
<Accordion.Panel>
<Stack py="xs" ml="md" spacing={4}>
<Checkbox
label="Download unread chapters upon startup"
size="md"
checked={OnStartUpDownloadUnread}
onChange={(e) =>
updateGeneralSetting(GeneralSetting.OnStartUpDownloadUnread, e.target.checked)
}
/>
<Checkbox
label="Download unread chapters upon loading specific manga details page"
size="md"
checked={OnSeriesDetailsDownloadUnread}
onChange={(e) =>
updateGeneralSetting(
GeneralSetting.OnSeriesDetailsDownloadUnread,
e.target.checked
)
}
/>
<Checkbox
label="Download unread chapters upon scrolling between chapters"
size="md"
checked={OnScrollingChaptersDownloadUnread}
onChange={(e) =>
updateGeneralSetting(
GeneralSetting.OnScrollingChaptersDownloadUnread,
e.target.checked
)
}
/>
<Text>how many unread chapters to keep downloaded</Text>
<NumberInput
disabled={
!OnStartUpDownloadUnread &&
!OnSeriesDetailsDownloadUnread &&
!OnScrollingChaptersDownloadUnread
}
min={1}
value={OnStartDownloadUnreadCount}
onChange={(value) =>
updateGeneralSetting(GeneralSetting.OnStartUpDownloadUnreadCount, value)
}
/>
<br />
</Stack>
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item value="delete">
<Accordion.Control>Auto Delete</Accordion.Control>
<Accordion.Panel>
<Stack py="xs" ml="md" spacing={4}>
<Checkbox
label="Delete read chapters upon startup"
size="md"
checked={OnStartUpDeleteRead}
onChange={(e) =>
updateGeneralSetting(GeneralSetting.OnStartUpDeleteRead, e.target.checked)
}
/>
<Checkbox
label="Delete read chapters upon loading specific manga details page"
size="md"
checked={OnSeriesDetailsDeleteRead}
onChange={(e) =>
updateGeneralSetting(GeneralSetting.OnSeriesDetailsDeleteRead, e.target.checked)
}
/>
</Stack>
</Accordion.Panel>
</Accordion.Item>
</Accordion>
</>
);
};

export default GeneralSettings;

export async function getDefaultDownloadDir(): Promise<any> {
return ipcRenderer.invoke(ipcChannels.GET_PATH.DEFAULT_DOWNLOADS_DIR);
}
99 changes: 97 additions & 2 deletions src/features/library/chapterDownloadUtils.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { Chapter, Series } from '@tiyo/common';
import { Chapter, LanguageKey, Series } from '@tiyo/common';
import { downloaderClient, DownloadTask } from '../../services/downloader';
import { getChapterDownloaded } from '../../util/filesystem';
import {
deleteDownloadedChapter,
getChapterDownloaded,
getChaptersDownloaded,
} from '../../util/filesystem';
import library from '../../services/library';

export async function downloadNextX(
chapterList: Chapter[],
@@ -78,3 +83,93 @@ export async function downloadAll(
);
downloaderClient.start();
}

/**
* The function `DownloadUnreadChapters` downloads a specified number of unread chapters from a list of
* series, filtering out already downloaded chapters.
* @param {Series[]} seriesList - An array of objects representing a list of series. Each series object
* should have properties like `sourceId`, `numberUnread`, and `id`.
* @param {string} downloadsDir - The `downloadsDir` parameter is a string that represents the
* directory where the downloaded chapters will be saved.
* @param {number} [count=1] - The `count` parameter specifies the number of unread chapters to
* download for each series. By default, it is set to 1, meaning it will download the latest unread
* chapter. However, you can provide a different value to download a specific number of unread
* chapters.
* @param {Chapter[]} serieChapters - An array of Chapter objects representing the chapters of a
* series.
* @returns The function does not have a return statement.
*/
export async function DownloadUnreadChapters(
seriesList: Series[],
downloadsDir: string,
chapterLanguages: LanguageKey[],
notification = true,
count = 1
) {
seriesList
.filter((series) => series.numberUnread > 0 && series.id)
.forEach(async (series) => {
library
.validFilePath(series.sourceId)
.then(async (result) => {
if (result === false) {
const serieChapters = library
.fetchChapters(series.id!)
.filter((x) => !x.read)
.filter((x) => chapterLanguages.includes(x.languageKey))
.sort((a, b) => parseFloat(a.chapterNumber) - parseFloat(b.chapterNumber))
.slice(0, count);

const nonDownloadedChapters = await Promise.all(
serieChapters.map(async (x) => {
const r = await getChapterDownloaded(series, x, downloadsDir);
return r !== true ? x : null;
})
);

const filteredNonDownloadedChapters = nonDownloadedChapters.filter(Boolean);

downloaderClient.add(
filteredNonDownloadedChapters.map(
(chapter) =>
({
chapter,
series,
downloadsDir,
} as DownloadTask)
)
);
downloaderClient.start(notification);
await new Promise((resolve) => setTimeout(resolve, 1000));
}
})
.catch((e: Error) => console.error(e));
});
}

/**
* The function `DeleteReadChapters` deletes downloaded chapters for a list of series that have been
* marked as read.
* @param {Series[]} seriesList - An array of objects representing a list of series. Each object in the
* array should have an "id" property.
* @param {string} downloadsDir - The `downloadsDir` parameter is a string that represents the
* directory where the downloaded chapters are stored.
* @param {Chapter[]} [serieChapters] - An optional array of Chapter objects representing the chapters
* of a series.
* @returns The function does not have a return statement.
*/
export async function DeleteReadChapters(seriesList: Series[], downloadsDir: string) {
seriesList
.filter((series) => series.id)
.forEach(async (series) => {
const serieChapters = library.fetchChapters(series.id!).filter((x) => x.read);

const downloadedChapters = await getChaptersDownloaded(series, serieChapters, downloadsDir);

const DownloadedChapters = serieChapters.filter((chapter) => downloadedChapters[chapter.id!]);

DownloadedChapters.forEach((x) => {
deleteDownloadedChapter(series, x, downloadsDir);
});
});
}
21 changes: 21 additions & 0 deletions src/models/types.ts
Original file line number Diff line number Diff line change
@@ -98,6 +98,13 @@ export enum GeneralSetting {
ChapterListVolOrder = 'ChapterListVolOrder',
ChapterListChOrder = 'ChapterListChOrder',
ChapterListPageSize = 'ChapterListPageSize',

OnStartUpDownloadUnread = 'OnStartUpDownloadUnread',
OnSeriesDetailsDownloadUnread = 'OnSeriesDetailsDownloadUnread',
OnScrollingChaptersDownloadUnread = 'OnScrollingChaptersDownloadUnread',
OnStartUpDownloadUnreadCount = 'OnStartUpDownloadUnreadCount',
OnStartUpDeleteRead = 'OnStartUpDeleteRead',
OnSeriesDetailsDeleteRead = 'OnSeriesDetailsDeleteRead',
}

export enum ProgressFilter {
@@ -237,6 +244,13 @@ export const SettingTypes = {
[TrackerSetting.TrackerAutoUpdate]: SettingType.BOOLEAN,

[IntegrationSetting.DiscordPresenceEnabled]: SettingType.BOOLEAN,

[GeneralSetting.OnStartUpDownloadUnread]: SettingType.BOOLEAN,
[GeneralSetting.OnSeriesDetailsDownloadUnread]: SettingType.BOOLEAN,
[GeneralSetting.OnScrollingChaptersDownloadUnread]: SettingType.BOOLEAN,
[GeneralSetting.OnStartUpDownloadUnreadCount]: SettingType.NUMBER,
[GeneralSetting.OnStartUpDeleteRead]: SettingType.BOOLEAN,
[GeneralSetting.OnSeriesDetailsDeleteRead]: SettingType.BOOLEAN,
};

export const DefaultSettings = {
@@ -290,4 +304,11 @@ export const DefaultSettings = {
[TrackerSetting.TrackerAutoUpdate]: true,

[IntegrationSetting.DiscordPresenceEnabled]: false,

[GeneralSetting.OnStartUpDownloadUnread]: false,
[GeneralSetting.OnSeriesDetailsDownloadUnread]: false,
[GeneralSetting.OnScrollingChaptersDownloadUnread]: false,
[GeneralSetting.OnStartUpDownloadUnreadCount]: 0,
[GeneralSetting.OnStartUpDeleteRead]: false,
[GeneralSetting.OnSeriesDetailsDeleteRead]: false,
};
6 changes: 4 additions & 2 deletions src/services/downloader.ts
Original file line number Diff line number Diff line change
@@ -99,7 +99,7 @@ class DownloaderClient {
this.setDownloadErrors([...this.downloadErrors, downloadError]);
};

start = async () => {
start = async (notification = true) => {
if (this.running) return;

if (this.queue.length === 0) {
@@ -109,7 +109,9 @@ class DownloaderClient {

const startingQueueSize = this.queue.length;
const notificationId = uuidv4();
showNotification({ id: notificationId, message: 'Starting download...', loading: true });
if (notification) {
showNotification({ id: notificationId, message: 'Starting download...', loading: true });
}

this.setRunning(true);
let tasksCompleted = 0;
28 changes: 28 additions & 0 deletions src/services/library.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Chapter, Series } from '@tiyo/common';
import { v4 as uuidv4 } from 'uuid';
import fs from 'fs';
import persistantStore from '../util/persistantStore';
import storeKeys from '../constants/storeKeys.json';
import { Category } from '../models/types';
@@ -105,6 +106,31 @@ const removeCategory = (categoryId: string): void => {
);
};

const validURL = (str: string): boolean => {
const pattern = new RegExp(
'^(https?:\\/\\/)?' + // protocol
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name
'((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path
'(\\?[;&a-z\\d%_.~+=-]*)?' + // query string
'(\\#[-a-z\\d_]*)?$',
'i'
); // fragment locator
return !!pattern.test(str);
};

const validFilePath = async (str: string): Promise<boolean> => {
return new Promise((resolve) => {
fs.access(str, fs.constants.F_OK, (err) => {
if (err) {
resolve(false);
} else {
resolve(true);
}
});
});
};

export default {
fetchSeriesList,
fetchSeries,
@@ -117,4 +143,6 @@ export default {
fetchCategoryList,
upsertCategory,
removeCategory,
validURL,
validFilePath,
};
7 changes: 7 additions & 0 deletions src/state/settingStates.ts
Original file line number Diff line number Diff line change
@@ -104,4 +104,11 @@ export const optimizeContrastState = atomFromSetting<boolean>(ReaderSetting.Opti
export const trackerAutoUpdateState = atomFromSetting<boolean>(TrackerSetting.TrackerAutoUpdate);

export const discordPresenceEnabledState = atomFromSetting<boolean>(IntegrationSetting.DiscordPresenceEnabled);

export const OnStartUpDownloadUnreadState = atomFromSetting<boolean>(GeneralSetting.OnStartUpDownloadUnread);
export const OnSeriesDetailsDownloadUnreadState = atomFromSetting<boolean>(GeneralSetting.OnSeriesDetailsDownloadUnread);
export const OnScrollingChaptersDownloadUnreadState = atomFromSetting<boolean>(GeneralSetting.OnScrollingChaptersDownloadUnread);
export const OnStartDownloadUnreadCountState = atomFromSetting<number>(GeneralSetting.OnStartUpDownloadUnreadCount);
export const OnStartUpDeleteReadState = atomFromSetting<boolean>(GeneralSetting.OnStartUpDeleteRead);
export const OnSeriesDetailsDeleteReadState = atomFromSetting<boolean>(GeneralSetting.OnSeriesDetailsDeleteRead);
/* eslint-enable */

Unchanged files with check annotations Beta

setColorScheme(value || (colorScheme === 'dark' ? 'light' : 'dark'));
const theme = useMantineTheme();
const toggleThemeSwitch = () => (

Check warning on line 195 in src/App.tsx

GitHub Actions / build (18, macos-latest)

'toggleThemeSwitch' is assigned a value but never used
<Switch
size="sm"
color={colorScheme === 'dark' ? theme.colors.dark[8] : theme.colors.gray[0]}
// eslint-disable-next-line @typescript-eslint/ban-types
type Props = {};
const DownloadQueue: React.FC<Props> = (props: Props) => {

Check warning on line 21 in src/components/downloads/DownloadQueue.tsx

GitHub Actions / build (18, macos-latest)

'props' is defined but never used
const queue = useRecoilValue(queueState);
const currentTask = useRecoilValue(currentTaskState);
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface Props {}
const Downloads: React.FC<Props> = (props: Props) => {

Check warning on line 9 in src/components/downloads/Downloads.tsx

GitHub Actions / build (18, macos-latest)

'props' is defined but never used
return (
<>
<DownloadQueue />
// eslint-disable-next-line @typescript-eslint/ban-types
type Props = {};
const MyDownloads: React.FC<Props> = (props: Props) => {

Check warning on line 20 in src/components/downloads/MyDownloads.tsx

GitHub Actions / build (18, macos-latest)

'props' is defined but never used
const [seriesList, setSeriesList] = useState<Series[]>([]);
const [chapterLists, setChapterLists] = useState<{ [key: string]: Chapter[] }>({});
const [checkedChapters, setCheckedChapters] = useState<string[]>([]);
step?: AppLoadStep;
};
const AppLoading: React.FC<Props> = (props: Props) => {

Check warning on line 13 in src/components/general/AppLoading.tsx

GitHub Actions / build (18, macos-latest)

'props' is defined but never used
return (
<div className={styles.container}>
<Image className={styles.logo} src={logo} width={96} height={96} />
</td>
<td>
<Group position="right" spacing="xs" noWrap>
{chapterDownloadStatuses[chapter.id!] ? (

Check warning on line 113 in src/components/library/ChapterTableRow.tsx

GitHub Actions / build (18, macos-latest)

Forbidden non-null assertion
<ActionIcon disabled>
<IconFileCheck size={16} />
</ActionIcon>