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/README.md +1 -1
- package/dist/index.js +378 -341
- package/package.json +1 -1
- package/src/api/index.ts +6 -32
- package/src/api/provider.ts +7 -0
- package/src/api/{bunkr.ts → providers/bunkr.ts} +15 -5
- package/src/api/{coomer-api.ts → providers/coomer.ts} +49 -34
- package/src/api/{gofile.ts → providers/gofile.ts} +20 -11
- package/src/api/providers/plainfile.ts +17 -0
- package/src/api/{nsfw.xxx.ts → providers/reddit.ts} +20 -10
- package/src/api/resolver.ts +23 -0
- package/src/cli/ui/app.tsx +12 -13
- package/src/cli/ui/components/file.tsx +7 -3
- package/src/cli/ui/components/filelist-state.tsx +48 -0
- package/src/cli/ui/components/filelist.tsx +10 -46
- package/src/cli/ui/components/index.ts +2 -1
- package/src/cli/ui/components/preview.tsx +1 -1
- package/src/cli/ui/hooks/downloader.ts +20 -15
- package/src/cli/ui/hooks/input.ts +0 -1
- package/src/cli/ui/store/index.ts +1 -1
- package/src/{services → core}/downloader.ts +10 -7
- package/src/core/file.ts +29 -0
- package/src/{services/file.ts → core/filelist.ts} +7 -33
- package/src/core/index.ts +3 -0
- package/src/index.ts +3 -3
- package/src/api/plain-curl.ts +0 -9
- /package/src/{logger/index.ts → utils/logger.ts} +0 -0
package/package.json
CHANGED
package/src/api/index.ts
CHANGED
|
@@ -1,32 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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';
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as cheerio from 'cheerio';
|
|
2
2
|
import { fetch } from 'undici';
|
|
3
|
-
import { CoomerFile
|
|
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) =>
|
|
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
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
type
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
|
57
|
-
|
|
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
|
-
|
|
60
|
-
|
|
67
|
+
const token = await getToken();
|
|
68
|
+
const websiteToken = await getWebsiteToken();
|
|
61
69
|
|
|
62
|
-
|
|
63
|
-
|
|
70
|
+
const filelist = await getFolderFiles(id, token, websiteToken);
|
|
71
|
+
filelist.dirName = `gofile-${id}`;
|
|
64
72
|
|
|
65
|
-
|
|
73
|
+
setGlobalHeaders({ Cookie: `accountToken=${token}` });
|
|
66
74
|
|
|
67
|
-
|
|
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
|
|
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 =
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
+
}
|
package/src/cli/ui/app.tsx
CHANGED
|
@@ -1,38 +1,37 @@
|
|
|
1
1
|
import { Box } from 'ink';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { CoomerFileList } from '../../
|
|
4
|
-
import {
|
|
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
|
-
{!
|
|
21
|
+
{!(filelist instanceof CoomerFileList) ? (
|
|
21
22
|
<Loading />
|
|
22
23
|
) : (
|
|
23
24
|
<>
|
|
24
25
|
<Box>
|
|
25
26
|
<Box>
|
|
26
|
-
<FileListStateBox
|
|
27
|
+
<FileListStateBox />
|
|
27
28
|
</Box>
|
|
28
29
|
<Box flexBasis={30}>
|
|
29
30
|
<KeyboardControlsInfo />
|
|
30
31
|
</Box>
|
|
31
32
|
</Box>
|
|
32
33
|
|
|
33
|
-
|
|
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 '../../../
|
|
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
|
|
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 {
|
|
2
|
+
import type { CoomerFile } from '../../../core';
|
|
3
|
+
import { useDownloaderHook } from '../hooks/downloader';
|
|
4
|
+
import { FileBox } from './file';
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
filelist
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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 {
|
|
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 '../../../
|
|
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 {
|
|
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
|
|
8
|
+
const versionRef = useRef(0);
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
type
|
|
16
|
-
|
|
17
|
-
|
|
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);
|