coomer-downloader 3.4.0 → 3.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coomer-downloader",
3
- "version": "3.4.0",
3
+ "version": "3.4.2",
4
4
  "author": "smartacephal",
5
5
  "license": "MIT",
6
6
  "description": "Downloads images/videos from Coomer/Kemono, Bunkr, GoFile, Reddit-NSFW user posts",
package/src/api/index.ts CHANGED
@@ -1,32 +1,6 @@
1
- import type { CoomerFileList } from '../services/file';
2
- import { getBunkrData } from './bunkr';
3
- import { getCoomerData } from './coomer-api';
4
- import { getGofileData } from './gofile';
5
- import { getRedditData } from './nsfw.xxx';
6
- import { getPlainFileData } from './plain-curl';
7
-
8
- export async function apiHandler(url_: string): Promise<CoomerFileList> {
9
- const url = new URL(url_);
10
-
11
- if (/^u\/\w+$/.test(url.origin)) {
12
- return getRedditData(url.href);
13
- }
14
-
15
- if (/coomer|kemono/.test(url.origin)) {
16
- return getCoomerData(url.href);
17
- }
18
-
19
- if (/bunkr/.test(url.origin)) {
20
- return getBunkrData(url.href);
21
- }
22
-
23
- if (/gofile\.io/.test(url.origin)) {
24
- return getGofileData(url.href);
25
- }
26
-
27
- if (/\.\w+/.test(url.pathname)) {
28
- return getPlainFileData(url.href);
29
- }
30
-
31
- throw Error('Invalid URL');
32
- }
1
+ export { BunkrAPI } from './providers/bunkr';
2
+ export { CoomerAPI } from './providers/coomer';
3
+ export { GofileAPI } from './providers/gofile';
4
+ export { PlainFileAPI } from './providers/plainfile';
5
+ export { RedditAPI } from './providers/reddit';
6
+ export { resolveAPI } from './resolver';
@@ -0,0 +1,7 @@
1
+ import type { CoomerFileList } from '../core/filelist';
2
+
3
+ export interface ProviderAPI {
4
+ fixURL?(url: string, retries: number): string;
5
+ testURL(url: URL): boolean;
6
+ getData(url: string): Promise<CoomerFileList>;
7
+ }
@@ -1,6 +1,8 @@
1
1
  import * as cheerio from 'cheerio';
2
2
  import { fetch } from 'undici';
3
- import { CoomerFile, CoomerFileList } from '../services/file';
3
+ import { CoomerFile } from '../../core/file';
4
+ import { CoomerFileList } from '../../core/filelist';
5
+ import type { ProviderAPI } from '../provider';
4
6
 
5
7
  type EncData = { url: string; timestamp: number };
6
8
 
@@ -18,7 +20,9 @@ function decryptEncryptedUrl(encryptionData: EncData) {
18
20
  const encryptedUrlBuffer = Buffer.from(encryptionData.url, 'base64');
19
21
  const secretKeyBuffer = Buffer.from(secretKey, 'utf-8');
20
22
  return Array.from(encryptedUrlBuffer)
21
- .map((byte, i) => String.fromCharCode(byte ^ secretKeyBuffer[i % secretKeyBuffer.length]))
23
+ .map((byte, i) =>
24
+ String.fromCharCode(byte ^ secretKeyBuffer[i % secretKeyBuffer.length]),
25
+ )
22
26
  .join('');
23
27
  }
24
28
 
@@ -61,7 +65,13 @@ async function getGalleryFiles(url: string): Promise<CoomerFileList> {
61
65
  return filelist;
62
66
  }
63
67
 
64
- export async function getBunkrData(url: string): Promise<CoomerFileList> {
65
- const filelist = await getGalleryFiles(url);
66
- return filelist;
68
+ export class BunkrAPI implements ProviderAPI {
69
+ public testURL(url: URL) {
70
+ return /bunkr/.test(url.origin);
71
+ }
72
+
73
+ public async getData(url: string): Promise<CoomerFileList> {
74
+ const filelist = await getGalleryFiles(url);
75
+ return filelist;
76
+ }
67
77
  }
@@ -1,33 +1,27 @@
1
- import { CoomerFile, CoomerFileList } from '../services/file';
2
- import { isImage } from '../utils/mediatypes';
3
- import { fetchWithGlobalHeader, setGlobalHeaders } from '../utils/requests';
4
-
5
- type CoomerAPIUser = { domain: string; service: string; id: string; name?: string };
6
- type CoomerAPIUserData = { name: string };
7
- type CoomerAPIFile = { path: string; name: string };
8
- type CoomerAPIPost = {
9
- title: string;
10
- content: string;
11
- published: string;
12
- attachments: CoomerAPIFile[];
13
- file: CoomerAPIFile;
14
- };
15
-
16
- const SERVERS = ['n1', 'n2', 'n3', 'n4'];
17
-
18
- export function tryFixCoomerUrl(url: string, attempts: number) {
19
- if (attempts < 2 && isImage(url)) {
20
- return url.replace(/\/data\//, '/thumbnail/data/').replace(/n\d\./, 'img.');
21
- }
22
- const server = url.match(/n\d\./)?.[0].slice(0, 2) as string;
23
- const i = SERVERS.indexOf(server);
24
- if (i !== -1) {
25
- const newServer = SERVERS[(i + 1) % SERVERS.length];
26
- return url.replace(/n\d./, `${newServer}.`);
27
- }
28
- return url;
1
+ import { CoomerFile } from '../../core/file';
2
+ import { CoomerFileList } from '../../core/filelist';
3
+ import { isImage } from '../../utils/mediatypes';
4
+ import { fetchWithGlobalHeader, setGlobalHeaders } from '../../utils/requests';
5
+ import type { ProviderAPI } from '../provider';
6
+
7
+ interface CoomerServiceAPI {
8
+ user: { domain: string; service: string; id: string; name?: string };
9
+ userData: { name: string };
10
+ file: { path: string; name: string };
11
+ post: {
12
+ title: string;
13
+ content: string;
14
+ published: string;
15
+ attachments: CoomerServiceAPI['file'][];
16
+ file: CoomerServiceAPI['file'];
17
+ };
29
18
  }
30
19
 
20
+ type CoomerAPIUser = CoomerServiceAPI['user'];
21
+ type CoomerAPIUserData = CoomerServiceAPI['userData'];
22
+ type CoomerAPIFile = CoomerServiceAPI['file'];
23
+ type CoomerAPIPost = CoomerServiceAPI['post'];
24
+
31
25
  async function getUserProfileData(user: CoomerAPIUser): Promise<CoomerAPIUserData> {
32
26
  const url = `${user.domain}/api/v1/${user.service}/user/${user.id}/profile`;
33
27
  const result = await fetchWithGlobalHeader(url).then((r) => r.json());
@@ -100,10 +94,31 @@ async function parseUser(url: string): Promise<CoomerAPIUser> {
100
94
  return { domain, service, id, name };
101
95
  }
102
96
 
103
- export async function getCoomerData(url: string): Promise<CoomerFileList> {
104
- setGlobalHeaders({ accept: 'text/css' });
105
- const user = await parseUser(url);
106
- const filelist = await getUserFiles(user);
107
- filelist.dirName = `${user.name}-${user.service}`;
108
- return filelist;
97
+ export class CoomerAPI implements ProviderAPI {
98
+ private static readonly SERVERS = ['n1', 'n2', 'n3', 'n4'];
99
+
100
+ public fixURL(url: string, retries: number): string {
101
+ if (retries < 2 && isImage(url)) {
102
+ return url.replace(/\/data\//, '/thumbnail/data/').replace(/n\d\./, 'img.');
103
+ }
104
+ const server = url.match(/n\d\./)?.[0].slice(0, 2) as string;
105
+ const i = CoomerAPI.SERVERS.indexOf(server);
106
+ if (i !== -1) {
107
+ const newServer = CoomerAPI.SERVERS[(i + 1) % CoomerAPI.SERVERS.length];
108
+ return url.replace(/n\d./, `${newServer}.`);
109
+ }
110
+ return url;
111
+ }
112
+
113
+ public testURL(url: URL) {
114
+ return /coomer|kemono/.test(url.origin);
115
+ }
116
+
117
+ public async getData(url: string): Promise<CoomerFileList> {
118
+ setGlobalHeaders({ accept: 'text/css' });
119
+ const user = await parseUser(url);
120
+ const filelist = await getUserFiles(user);
121
+ filelist.dirName = `${user.name}-${user.service}`;
122
+ return filelist;
123
+ }
109
124
  }
@@ -1,6 +1,8 @@
1
1
  import { fetch } from 'undici';
2
- import { CoomerFile, CoomerFileList } from '../services/file';
3
- import { setGlobalHeaders } from '../utils/requests';
2
+ import { CoomerFile } from '../../core/file';
3
+ import { CoomerFileList } from '../../core/filelist';
4
+ import { setGlobalHeaders } from '../../utils/requests';
5
+ import type { ProviderAPI } from '../provider';
4
6
 
5
7
  type GoFileAPIToken = { status: string; data: { token: string } };
6
8
  type GoFileAPIFilelist = { data: { children: { link: string; name: string }[] } };
@@ -13,7 +15,8 @@ async function getToken(): Promise<string> {
13
15
  if (data.status === 'ok') {
14
16
  return data.data.token;
15
17
  }
16
- throw new Error('cannot get token');
18
+
19
+ throw new Error('Token Not Found');
17
20
  }
18
21
 
19
22
  async function getWebsiteToken() {
@@ -53,16 +56,22 @@ async function getFolderFiles(
53
56
  return new CoomerFileList(files);
54
57
  }
55
58
 
56
- export async function getGofileData(url: string): Promise<CoomerFileList> {
57
- const id = url.match(/gofile.io\/d\/(\w+)/)?.[1] as string;
59
+ export class GofileAPI implements ProviderAPI {
60
+ public testURL(url: URL) {
61
+ return /gofile\.io/.test(url.origin);
62
+ }
63
+
64
+ public async getData(url: string): Promise<CoomerFileList> {
65
+ const id = url.match(/gofile.io\/d\/(\w+)/)?.[1] as string;
58
66
 
59
- const token = await getToken();
60
- const websiteToken = await getWebsiteToken();
67
+ const token = await getToken();
68
+ const websiteToken = await getWebsiteToken();
61
69
 
62
- const filelist = await getFolderFiles(id, token, websiteToken);
63
- filelist.dirName = `gofile-${id}`;
70
+ const filelist = await getFolderFiles(id, token, websiteToken);
71
+ filelist.dirName = `gofile-${id}`;
64
72
 
65
- setGlobalHeaders({ Cookie: `accountToken=${token}` });
73
+ setGlobalHeaders({ Cookie: `accountToken=${token}` });
66
74
 
67
- return filelist;
75
+ return filelist;
76
+ }
68
77
  }
@@ -0,0 +1,17 @@
1
+ import { CoomerFile } from '../../core/file';
2
+ import { CoomerFileList } from '../../core/filelist';
3
+ import type { ProviderAPI } from '../provider';
4
+
5
+ export class PlainFileAPI implements ProviderAPI {
6
+ public testURL(url: URL) {
7
+ return /\.\w+/.test(url.pathname);
8
+ }
9
+
10
+ public async getData(url: string): Promise<CoomerFileList> {
11
+ const name = url.split('/').pop() as string;
12
+ const file = CoomerFile.from({ name, url });
13
+ const filelist = new CoomerFileList([file]);
14
+ filelist.dirName = '';
15
+ return filelist;
16
+ }
17
+ }
@@ -1,6 +1,9 @@
1
1
  import * as cheerio from 'cheerio';
2
2
  import { fetch } from 'undici';
3
- import { CoomerFile, CoomerFileList } from '../services/file';
3
+ import { CoomerFile } from '../../core/file';
4
+ import { CoomerFileList } from '../../core/filelist';
5
+ import logger from '../../utils/logger';
6
+ import type { ProviderAPI } from '../provider';
4
7
 
5
8
  async function getUserPage(user: string, offset: number) {
6
9
  const url = `https://nsfw.xxx/page/${offset}?nsfw[]=0&types[]=image&types[]=video&types[]=gallery&slider=1&jsload=1&user=${user}&_=${Date.now()}`;
@@ -19,6 +22,7 @@ async function getUserPosts(user: string): Promise<string[]> {
19
22
  .get()
20
23
  .filter((href) => href?.startsWith('https://nsfw.xxx/post'));
21
24
 
25
+ logger.debug({ count: posts.length });
22
26
  posts.push(...newPosts);
23
27
  }
24
28
  return posts;
@@ -38,23 +42,29 @@ async function getPostsData(posts: string[]): Promise<CoomerFileList> {
38
42
  if (!src) continue;
39
43
 
40
44
  const slug = post.split('post/')[1].split('?')[0];
41
- const date = $('.sh-section .sh-section__passed').first().text().replace(/ /g, '-') || '';
45
+ const date =
46
+ $('.sh-section .sh-section__passed').first().text().replace(/ /g, '-') || '';
42
47
 
43
48
  const ext = src.split('.').pop();
44
49
  const name = `${slug}-${date}.${ext}`;
45
50
 
51
+ logger.debug({ hehe: filelist.files.length, src });
46
52
  filelist.files.push(CoomerFile.from({ name, url: src }));
47
53
  }
48
54
 
49
55
  return filelist;
50
56
  }
51
57
 
52
- export async function getRedditData(url: string): Promise<CoomerFileList> {
53
- const user = url.match(/u\/(\w+)/)?.[1] as string;
54
- console.log('Fetching user posts...');
55
- const posts = await getUserPosts(user);
56
- console.log('Fetching posts data...');
57
- const filelist = await getPostsData(posts);
58
- filelist.dirName = `${user}-reddit`;
59
- return filelist;
58
+ export class RedditAPI implements ProviderAPI {
59
+ public testURL(url: URL) {
60
+ return /^\/user\/[\w-]+$/.test(url.pathname);
61
+ }
62
+
63
+ public async getData(url: string): Promise<CoomerFileList> {
64
+ const user = url.match(/^\/user\/([\w-]+)/)?.[1] as string;
65
+ const posts = await getUserPosts(user);
66
+ const filelist = await getPostsData(posts);
67
+ filelist.dirName = `${user}-reddit`;
68
+ return filelist;
69
+ }
60
70
  }
@@ -0,0 +1,23 @@
1
+ import type { CoomerFileList } from '../core/filelist';
2
+ import { BunkrAPI } from './providers/bunkr';
3
+ import { CoomerAPI } from './providers/coomer';
4
+ import { GofileAPI } from './providers/gofile';
5
+ import { PlainFileAPI } from './providers/plainfile';
6
+ import { RedditAPI } from './providers/reddit';
7
+
8
+ const providers = [RedditAPI, CoomerAPI, BunkrAPI, GofileAPI, PlainFileAPI];
9
+
10
+ export async function resolveAPI(url_: string): Promise<CoomerFileList> {
11
+ const url = new URL(url_);
12
+
13
+ for (const p of providers) {
14
+ const provider = new p();
15
+ if (provider.testURL(url)) {
16
+ const filelist = await provider.getData(url.toString());
17
+ filelist.provider = provider;
18
+ return filelist;
19
+ }
20
+ }
21
+
22
+ throw Error('Invalid URL');
23
+ }
@@ -1,38 +1,37 @@
1
1
  import { Box } from 'ink';
2
2
  import React from 'react';
3
- import { CoomerFileList } from '../../services/file';
4
- import { FileBox, FileListStateBox, KeyboardControlsInfo, Loading, TitleBar } from './components';
3
+ import { CoomerFileList } from '../../core';
4
+ import {
5
+ FileListBox,
6
+ FileListStateBox,
7
+ KeyboardControlsInfo,
8
+ Loading,
9
+ TitleBar,
10
+ } from './components';
5
11
  import { useDownloaderHook } from './hooks/downloader';
6
12
  import { useInputHook } from './hooks/input';
7
- import { useInkStore } from './store';
8
13
 
9
14
  export function App() {
10
15
  useInputHook();
11
- useDownloaderHook();
12
-
13
- const downloader = useInkStore((state) => state.downloader);
14
- const filelist = downloader?.filelist;
15
- const isFilelist = filelist instanceof CoomerFileList;
16
+ const filelist = useDownloaderHook(['FILES_DOWNLOADING_START']);
16
17
 
17
18
  return (
18
19
  <Box borderStyle="single" flexDirection="column" borderColor="blue" width={80}>
19
20
  <TitleBar />
20
- {!isFilelist ? (
21
+ {!(filelist instanceof CoomerFileList) ? (
21
22
  <Loading />
22
23
  ) : (
23
24
  <>
24
25
  <Box>
25
26
  <Box>
26
- <FileListStateBox filelist={filelist} />
27
+ <FileListStateBox />
27
28
  </Box>
28
29
  <Box flexBasis={30}>
29
30
  <KeyboardControlsInfo />
30
31
  </Box>
31
32
  </Box>
32
33
 
33
- {filelist.getActiveFiles().map((file) => {
34
- return <FileBox file={file} key={file.name} />;
35
- })}
34
+ <FileListBox />
36
35
  </>
37
36
  )}
38
37
  </Box>
@@ -1,7 +1,8 @@
1
1
  import { Box, Spacer, Text } from 'ink';
2
2
  import React from 'react';
3
- import type { CoomerFile } from '../../../services/file';
3
+ import type { CoomerFile } from '../../../core';
4
4
  import { b2mb } from '../../../utils/strings';
5
+ import { useDownloaderHook } from '../hooks/downloader';
5
6
  import { Preview } from './preview';
6
7
  import { Spinner } from './spinner';
7
8
 
@@ -9,8 +10,11 @@ interface FileBoxProps {
9
10
  file: CoomerFile;
10
11
  }
11
12
 
12
- export function FileBox({ file }: FileBoxProps) {
13
+ export const FileBox = React.memo(({ file }: FileBoxProps) => {
14
+ useDownloaderHook(['CHUNK_DOWNLOADING_UPDATE']);
15
+
13
16
  const percentage = Number((file.downloaded / (file.size as number)) * 100).toFixed(2);
17
+
14
18
  return (
15
19
  <>
16
20
  <Box
@@ -41,4 +45,4 @@ export function FileBox({ file }: FileBoxProps) {
41
45
  <Preview file={file} />
42
46
  </>
43
47
  );
44
- }
48
+ });
@@ -0,0 +1,48 @@
1
+ import { Box, Text } from 'ink';
2
+ import React from 'react';
3
+ import { useDownloaderHook } from '../hooks/downloader';
4
+
5
+ export function FileListStateBox() {
6
+ const filelist = useDownloaderHook(['FILE_DOWNLOADING_START', 'FILE_DOWNLOADING_END']);
7
+
8
+ return (
9
+ <Box
10
+ paddingX={1}
11
+ flexDirection="column"
12
+ borderStyle={'single'}
13
+ borderColor={'magenta'}
14
+ borderDimColor
15
+ >
16
+ <Box>
17
+ <Box marginRight={1}>
18
+ <Text color="cyanBright" dimColor>
19
+ Found:
20
+ </Text>
21
+ </Box>
22
+ <Text color="blue" dimColor wrap="wrap">
23
+ {filelist?.files.length}
24
+ </Text>
25
+ </Box>
26
+ <Box>
27
+ <Box marginRight={1}>
28
+ <Text color="cyanBright" dimColor>
29
+ Downloaded:
30
+ </Text>
31
+ </Box>
32
+ <Text color="blue" dimColor wrap="wrap"></Text>
33
+ </Box>
34
+ <Box>
35
+ <Box width={9}>
36
+ <Text color="cyanBright" dimColor>
37
+ Folder:
38
+ </Text>
39
+ </Box>
40
+ <Box flexGrow={1}>
41
+ <Text color="blue" dimColor wrap="truncate-middle">
42
+ {filelist?.dirPath}
43
+ </Text>
44
+ </Box>
45
+ </Box>
46
+ </Box>
47
+ );
48
+ }
@@ -1,52 +1,16 @@
1
- import { Box, Text } from 'ink';
2
1
  import React from 'react';
3
- import type { CoomerFileList } from '../../../services/file';
2
+ import type { CoomerFile } from '../../../core';
3
+ import { useDownloaderHook } from '../hooks/downloader';
4
+ import { FileBox } from './file';
4
5
 
5
- interface FileListStateBoxProps {
6
- filelist: CoomerFileList;
7
- }
6
+ export function FileListBox() {
7
+ const filelist = useDownloaderHook(['FILE_DOWNLOADING_START', 'FILE_DOWNLOADING_END']);
8
8
 
9
- export function FileListStateBox({ filelist }: FileListStateBoxProps) {
10
9
  return (
11
- <Box
12
- paddingX={1}
13
- flexDirection="column"
14
- borderStyle={'single'}
15
- borderColor={'magenta'}
16
- borderDimColor
17
- >
18
- <Box>
19
- <Box marginRight={1}>
20
- <Text color="cyanBright" dimColor>
21
- Found:
22
- </Text>
23
- </Box>
24
- <Text color="blue" dimColor wrap="wrap">
25
- {filelist.files.length}
26
- </Text>
27
- </Box>
28
- <Box>
29
- <Box marginRight={1}>
30
- <Text color="cyanBright" dimColor>
31
- Downloaded:
32
- </Text>
33
- </Box>
34
- <Text color="blue" dimColor wrap="wrap">
35
- {filelist.getDownloaded().length}
36
- </Text>
37
- </Box>
38
- <Box>
39
- <Box width={9}>
40
- <Text color="cyanBright" dimColor>
41
- Folder:
42
- </Text>
43
- </Box>
44
- <Box flexGrow={1}>
45
- <Text color="blue" dimColor wrap="truncate-middle">
46
- {filelist.dirPath}
47
- </Text>
48
- </Box>
49
- </Box>
50
- </Box>
10
+ <>
11
+ {filelist?.getActiveFiles().map((file: CoomerFile) => {
12
+ return <FileBox file={file} key={file.name} />;
13
+ })}
14
+ </>
51
15
  );
52
16
  }
@@ -1,5 +1,6 @@
1
1
  export { FileBox } from './file';
2
- export { FileListStateBox } from './filelist';
2
+ export { FileListBox } from './filelist';
3
+ export { FileListStateBox } from './filelist-state';
3
4
  export { KeyboardControlsInfo } from './keyboardinfo';
4
5
  export { Loading } from './loading';
5
6
  export { Spinner } from './spinner';
@@ -1,7 +1,7 @@
1
1
  import { Box } from 'ink';
2
2
  import Image, { TerminalInfoProvider } from 'ink-picture';
3
3
  import React from 'react';
4
- import type { CoomerFile } from '../../../services/file';
4
+ import type { CoomerFile } from '../../../core';
5
5
  import { isImage } from '../../../utils/mediatypes';
6
6
  import { useInkStore } from '../store';
7
7
 
@@ -1,21 +1,26 @@
1
- import { useEffect, useState } from 'react';
1
+ import { useRef, useSyncExternalStore } from 'react';
2
+ import type { DownloaderSubject, DownloaderSubjectSignal } from '../../../types';
2
3
  import { useInkStore } from '../store';
3
4
 
4
- export const useDownloaderHook = () => {
5
+ export const useDownloaderHook = (subjectEvents: DownloaderSubjectSignal[]) => {
5
6
  const downloader = useInkStore((state) => state.downloader);
6
- const filelist = downloader?.filelist;
7
7
 
8
- const [_, setHelper] = useState(0);
8
+ const versionRef = useRef(0);
9
9
 
10
- useEffect(() => {
11
- downloader?.subject.subscribe(({ type }) => {
12
- if (
13
- type === 'FILE_DOWNLOADING_START' ||
14
- type === 'FILE_DOWNLOADING_END' ||
15
- type === 'CHUNK_DOWNLOADING_UPDATE'
16
- ) {
17
- setHelper(Date.now());
18
- }
19
- });
20
- });
10
+ useSyncExternalStore(
11
+ (onStoreChange) => {
12
+ if (!downloader) return () => {};
13
+
14
+ const sub = downloader.subject.subscribe(({ type }: DownloaderSubject) => {
15
+ if (subjectEvents.includes(type)) {
16
+ versionRef.current++;
17
+ onStoreChange();
18
+ }
19
+ });
20
+ return () => sub.unsubscribe();
21
+ },
22
+ () => versionRef.current,
23
+ );
24
+
25
+ return downloader?.filelist;
21
26
  };
@@ -1,7 +1,6 @@
1
1
  import { useInput } from 'ink';
2
2
  import { useInkStore } from '../store';
3
3
 
4
- // problems with tsx watch
5
4
  export const useInputHook = () => {
6
5
  const downloader = useInkStore((state) => state.downloader);
7
6
  const switchPreview = useInkStore((state) => state.switchPreview);
@@ -1,5 +1,5 @@
1
1
  import { create } from 'zustand';
2
- import type { Downloader } from '../../../services/downloader';
2
+ import type { Downloader } from '../../../core';
3
3
 
4
4
  interface InkState {
5
5
  preview: boolean;