coomer-downloader 3.1.0 → 3.3.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/dist/index.js +574 -394
- package/package.json +13 -5
- package/src/api/bunkr.ts +16 -17
- package/src/api/coomer-api.ts +25 -25
- package/src/api/gofile.ts +18 -14
- package/src/api/index.ts +20 -16
- package/src/api/nsfw.xxx.ts +9 -10
- package/src/api/plain-curl.ts +7 -11
- package/src/{args-handler.ts → cli/args-handler.ts} +1 -4
- package/src/cli/ui/app.tsx +40 -0
- package/src/cli/ui/components/file.tsx +44 -0
- package/src/cli/ui/components/filelist.tsx +52 -0
- package/src/cli/ui/components/index.ts +6 -0
- package/src/cli/ui/components/keyboardinfo.tsx +41 -0
- package/src/cli/ui/components/loading.tsx +20 -0
- package/src/cli/ui/components/preview.tsx +32 -0
- package/src/cli/ui/components/spinner.tsx +28 -0
- package/src/cli/ui/components/titlebar.tsx +15 -0
- package/src/cli/ui/hooks/downloader.ts +21 -0
- package/src/cli/ui/hooks/input.ts +17 -0
- package/src/cli/ui/index.tsx +7 -0
- package/src/cli/ui/store/index.ts +19 -0
- package/src/index.ts +15 -21
- package/src/services/downloader.ts +141 -0
- package/src/services/file.ts +95 -0
- package/src/types/index.ts +12 -20
- package/src/utils/filters.ts +11 -14
- package/src/utils/promise.ts +0 -50
- package/src/utils/requests.ts +2 -2
- package/src/utils/strings.ts +1 -19
- package/src/utils/timer.ts +10 -9
- package/tsconfig.json +2 -1
- package/src/utils/downloader.ts +0 -102
- package/src/utils/index.ts +0 -11
- package/src/utils/multibar.ts +0 -62
- /package/src/utils/{files.ts → io.ts} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coomer-downloader",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.2",
|
|
4
4
|
"author": "smartacephal",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Downloads images/videos from Coomer/Kemono, Bunkr, GoFile, Reddit-NSFW user posts",
|
|
@@ -22,10 +22,11 @@
|
|
|
22
22
|
"type": "git",
|
|
23
23
|
"url": "git+https://github.com/smartacephale/coomer-downloader.git"
|
|
24
24
|
},
|
|
25
|
-
"main": "./src/index.
|
|
25
|
+
"main": "./src/index.tsx",
|
|
26
26
|
"type": "module",
|
|
27
27
|
"scripts": {
|
|
28
28
|
"start": "tsx ./src/index.ts",
|
|
29
|
+
"start-watch": "tsx watch ./src/index.ts",
|
|
29
30
|
"build": "node build.js",
|
|
30
31
|
"start-build": "npm run build && node ./dist/index.js"
|
|
31
32
|
},
|
|
@@ -38,18 +39,25 @@
|
|
|
38
39
|
},
|
|
39
40
|
"dependencies": {
|
|
40
41
|
"cheerio": "1.1.0",
|
|
41
|
-
"cli-
|
|
42
|
+
"cli-spinners": "^3.3.0",
|
|
42
43
|
"http-cookie-agent": "^6.0.0",
|
|
44
|
+
"ink": "^6.5.1",
|
|
45
|
+
"ink-picture": "^1.3.3",
|
|
46
|
+
"react": "^19.2.0",
|
|
43
47
|
"rxjs": "^7.8.2",
|
|
44
48
|
"tough-cookie": "5.1.2",
|
|
45
49
|
"undici": "^6.22.0",
|
|
46
|
-
"yargs": "^17.7.2"
|
|
50
|
+
"yargs": "^17.7.2",
|
|
51
|
+
"zustand": "^5.0.8"
|
|
47
52
|
},
|
|
48
53
|
"devDependencies": {
|
|
49
|
-
"@types/cli-progress": "^3.11.6",
|
|
50
54
|
"@types/node": "^24.10.0",
|
|
55
|
+
"@types/react": "^19.2.6",
|
|
51
56
|
"@types/yargs": "^17.0.34",
|
|
52
57
|
"esbuild": "^0.27.0",
|
|
58
|
+
"eslint-config-xo-react": "^0.27.0",
|
|
59
|
+
"eslint-plugin-react": "^7.32.2",
|
|
60
|
+
"eslint-plugin-react-hooks": "^4.6.0",
|
|
53
61
|
"tsx": "^4.20.6"
|
|
54
62
|
}
|
|
55
63
|
}
|
package/src/api/bunkr.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as cheerio from 'cheerio';
|
|
2
2
|
import { fetch } from 'undici';
|
|
3
|
-
import
|
|
4
|
-
import { testMediaType } from '../utils/index.js';
|
|
3
|
+
import { CoomerFile, CoomerFileList } from '../services/file';
|
|
5
4
|
|
|
6
5
|
type EncData = { url: string; timestamp: number };
|
|
7
6
|
|
|
@@ -23,46 +22,46 @@ function decryptEncryptedUrl(encryptionData: EncData) {
|
|
|
23
22
|
.join('');
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
async function getFileData(url: string, name: string) {
|
|
25
|
+
async function getFileData(url: string, name: string): Promise<CoomerFile> {
|
|
27
26
|
const slug = url.split('/').pop() as string;
|
|
28
27
|
const encryptionData = await getEncryptionData(slug);
|
|
29
28
|
const src = decryptEncryptedUrl(encryptionData);
|
|
30
|
-
return { name, url: src };
|
|
29
|
+
return CoomerFile.from({ name, url: src });
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
async function getGalleryFiles(url: string
|
|
34
|
-
const
|
|
32
|
+
async function getGalleryFiles(url: string): Promise<CoomerFileList> {
|
|
33
|
+
const filelist = new CoomerFileList();
|
|
35
34
|
const page = await fetch(url).then((r) => r.text());
|
|
36
35
|
const $ = cheerio.load(page);
|
|
37
|
-
const
|
|
36
|
+
const dirName = $('title').text();
|
|
37
|
+
filelist.dirName = `${dirName.split('|')[0].trim()}-bunkr`;
|
|
38
38
|
const url_ = new URL(url);
|
|
39
39
|
|
|
40
40
|
if (url_.pathname.startsWith('/f/')) {
|
|
41
41
|
const fileName = $('h1').text();
|
|
42
42
|
const singleFile = await getFileData(url, fileName);
|
|
43
|
-
|
|
44
|
-
return
|
|
43
|
+
filelist.files.push(singleFile);
|
|
44
|
+
return filelist;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
const fileNames = Array.from($('div[title]').map((_, e) => $(e).attr('title')));
|
|
48
48
|
|
|
49
|
-
const
|
|
49
|
+
const data = Array.from($('a').map((_, e) => $(e).attr('href')))
|
|
50
50
|
.filter((a) => /\/f\/\w+/.test(a))
|
|
51
51
|
.map((a, i) => ({
|
|
52
52
|
url: `${url_.origin}${a}`,
|
|
53
53
|
name: fileNames[i] || (url.split('/').pop() as string),
|
|
54
54
|
}));
|
|
55
55
|
|
|
56
|
-
for (const { name, url } of
|
|
56
|
+
for (const { name, url } of data) {
|
|
57
57
|
const res = await getFileData(url, name);
|
|
58
|
-
|
|
58
|
+
filelist.files.push(res);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
return
|
|
61
|
+
return filelist;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
export async function getBunkrData(url: string
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
return { dirName, files };
|
|
64
|
+
export async function getBunkrData(url: string): Promise<CoomerFileList> {
|
|
65
|
+
const filelist = await getGalleryFiles(url);
|
|
66
|
+
return filelist;
|
|
68
67
|
}
|
package/src/api/coomer-api.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
type
|
|
6
|
-
type
|
|
7
|
-
type
|
|
1
|
+
import { CoomerFile, CoomerFileList } from '../services/file';
|
|
2
|
+
import { isImage } from '../utils/filters';
|
|
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 = {
|
|
8
9
|
title: string;
|
|
9
10
|
content: string;
|
|
10
11
|
published: string;
|
|
11
|
-
attachments:
|
|
12
|
-
file:
|
|
12
|
+
attachments: CoomerAPIFile[];
|
|
13
|
+
file: CoomerAPIFile;
|
|
13
14
|
};
|
|
14
15
|
|
|
15
16
|
const SERVERS = ['n1', 'n2', 'n3', 'n4'];
|
|
@@ -27,19 +28,19 @@ export function tryFixCoomerUrl(url: string, attempts: number) {
|
|
|
27
28
|
return url;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
async function
|
|
31
|
+
async function getUserProfileData(user: CoomerAPIUser): Promise<CoomerAPIUserData> {
|
|
31
32
|
const url = `${user.domain}/api/v1/${user.service}/user/${user.id}/profile`;
|
|
32
33
|
const result = await fetchWithGlobalHeader(url).then((r) => r.json());
|
|
33
|
-
return result as
|
|
34
|
+
return result as CoomerAPIUserData;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
async function getUserPostsAPI(user:
|
|
37
|
+
async function getUserPostsAPI(user: CoomerAPIUser, offset: number): Promise<CoomerAPIPost[]> {
|
|
37
38
|
const url = `${user.domain}/api/v1/${user.service}/user/${user.id}/posts?o=${offset}`;
|
|
38
39
|
const posts = await fetchWithGlobalHeader(url).then((r) => r.json());
|
|
39
|
-
return posts as
|
|
40
|
+
return posts as CoomerAPIPost[];
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
export async function getUserFiles(user:
|
|
43
|
+
export async function getUserFiles(user: CoomerAPIUser): Promise<CoomerFileList> {
|
|
43
44
|
const userPosts = [];
|
|
44
45
|
|
|
45
46
|
const offset = 50;
|
|
@@ -49,7 +50,7 @@ export async function getUserFiles(user: CoomerUser, mediaType: MediaType): Prom
|
|
|
49
50
|
if (posts.length < 50) break;
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
const
|
|
53
|
+
const filelist = new CoomerFileList();
|
|
53
54
|
|
|
54
55
|
for (const p of userPosts) {
|
|
55
56
|
const title = p.title.match(/\w+/g)?.join(' ') || '';
|
|
@@ -59,35 +60,34 @@ export async function getUserFiles(user: CoomerUser, mediaType: MediaType): Prom
|
|
|
59
60
|
|
|
60
61
|
const postFiles = [...p.attachments, p.file]
|
|
61
62
|
.filter((f) => f.path)
|
|
62
|
-
.filter((f) => testMediaType(f.name, mediaType))
|
|
63
63
|
.map((f, i) => {
|
|
64
64
|
const ext = f.name.split('.').pop();
|
|
65
65
|
const name = `${datentitle} ${i + 1}.${ext}`;
|
|
66
66
|
const url = `${user.domain}/${f.path}`;
|
|
67
|
-
return { name, url, content };
|
|
67
|
+
return CoomerFile.from({ name, url, content });
|
|
68
68
|
});
|
|
69
69
|
|
|
70
|
-
files.push(...postFiles);
|
|
70
|
+
filelist.files.push(...postFiles);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
return
|
|
73
|
+
return filelist;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
async function parseUser(url: string): Promise<
|
|
76
|
+
async function parseUser(url: string): Promise<CoomerAPIUser> {
|
|
77
77
|
const [_, domain, service, id] = url.match(
|
|
78
78
|
/(https:\/\/\w+\.\w+)\/(\w+)\/user\/([\w|.|-]+)/,
|
|
79
79
|
) as RegExpMatchArray;
|
|
80
80
|
if (!domain || !service || !id) console.error('Invalid URL', url);
|
|
81
81
|
|
|
82
|
-
const { name } = await
|
|
82
|
+
const { name } = await getUserProfileData({ domain, service, id });
|
|
83
83
|
|
|
84
84
|
return { domain, service, id, name };
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
export async function getCoomerData(url: string
|
|
87
|
+
export async function getCoomerData(url: string): Promise<CoomerFileList> {
|
|
88
88
|
setGlobalHeaders({ accept: 'text/css' });
|
|
89
89
|
const user = await parseUser(url);
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
return
|
|
90
|
+
const filelist = await getUserFiles(user);
|
|
91
|
+
filelist.dirName = `${user.name}-${user.service}`;
|
|
92
|
+
return filelist;
|
|
93
93
|
}
|
package/src/api/gofile.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { fetch } from 'undici';
|
|
2
|
-
import
|
|
3
|
-
import { setGlobalHeaders
|
|
2
|
+
import { CoomerFile, CoomerFileList } from '../services/file';
|
|
3
|
+
import { setGlobalHeaders } from '../utils/requests';
|
|
4
4
|
|
|
5
5
|
type GoFileAPIToken = { status: string; data: { token: string } };
|
|
6
6
|
type GoFileAPIFilelist = { data: { children: { link: string; name: string }[] } };
|
|
@@ -26,7 +26,11 @@ async function getWebsiteToken() {
|
|
|
26
26
|
throw new Error('cannot get wt');
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
async function getFolderFiles(
|
|
29
|
+
async function getFolderFiles(
|
|
30
|
+
id: string,
|
|
31
|
+
token: string,
|
|
32
|
+
websiteToken: string,
|
|
33
|
+
): Promise<CoomerFileList> {
|
|
30
34
|
const url = `https://api.gofile.io/contents/${id}?wt=${websiteToken}&cache=true}`;
|
|
31
35
|
const response = await fetch(url, {
|
|
32
36
|
headers: {
|
|
@@ -39,26 +43,26 @@ async function getFolderFiles(id: string, token: string, websiteToken: string):
|
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
const data = (await response.json()) as GoFileAPIFilelist;
|
|
42
|
-
const files = Object.values(data.data.children).map((f) =>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
const files = Object.values(data.data.children).map((f) =>
|
|
47
|
+
CoomerFile.from({
|
|
48
|
+
url: f.link,
|
|
49
|
+
name: f.name,
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
46
52
|
|
|
47
|
-
return files;
|
|
53
|
+
return new CoomerFileList(files);
|
|
48
54
|
}
|
|
49
55
|
|
|
50
|
-
export async function getGofileData(url: string
|
|
56
|
+
export async function getGofileData(url: string): Promise<CoomerFileList> {
|
|
51
57
|
const id = url.match(/gofile.io\/d\/(\w+)/)?.[1] as string;
|
|
52
|
-
const dirName = `gofile-${id}`;
|
|
53
58
|
|
|
54
59
|
const token = await getToken();
|
|
55
60
|
const websiteToken = await getWebsiteToken();
|
|
56
61
|
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
);
|
|
62
|
+
const filelist = await getFolderFiles(id, token, websiteToken);
|
|
63
|
+
filelist.dirName = `gofile-${id}`;
|
|
60
64
|
|
|
61
65
|
setGlobalHeaders({ Cookie: `accountToken=${token}` });
|
|
62
66
|
|
|
63
|
-
return
|
|
67
|
+
return filelist;
|
|
64
68
|
}
|
package/src/api/index.ts
CHANGED
|
@@ -1,28 +1,32 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { CoomerFileList } from '../services/file';
|
|
2
2
|
import { getBunkrData } from './bunkr';
|
|
3
3
|
import { getCoomerData } from './coomer-api';
|
|
4
4
|
import { getGofileData } from './gofile';
|
|
5
5
|
import { getRedditData } from './nsfw.xxx';
|
|
6
6
|
import { getPlainFileData } from './plain-curl';
|
|
7
7
|
|
|
8
|
-
export async function apiHandler(
|
|
9
|
-
url
|
|
10
|
-
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
return getRedditData(url, mediaType);
|
|
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);
|
|
14
13
|
}
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
|
|
15
|
+
if (/coomer|kemono/.test(url.origin)) {
|
|
16
|
+
return getCoomerData(url.href);
|
|
17
17
|
}
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
|
|
19
|
+
if (/bunkr/.test(url.origin)) {
|
|
20
|
+
return getBunkrData(url.href);
|
|
20
21
|
}
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
|
|
23
|
+
if (/gofile\.io/.test(url.origin)) {
|
|
24
|
+
return getGofileData(url.href);
|
|
23
25
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
|
|
27
|
+
if (/\.\w+/.test(url.pathname)) {
|
|
28
|
+
return getPlainFileData(url.href);
|
|
26
29
|
}
|
|
27
|
-
|
|
30
|
+
|
|
31
|
+
throw Error('Invalid URL');
|
|
28
32
|
}
|
package/src/api/nsfw.xxx.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as cheerio from 'cheerio';
|
|
2
2
|
import { fetch } from 'undici';
|
|
3
|
-
import
|
|
4
|
-
import { testMediaType } from '../utils/index.js';
|
|
3
|
+
import { CoomerFile, CoomerFileList } from '../services/file';
|
|
5
4
|
|
|
6
5
|
async function getUserPage(user: string, offset: number) {
|
|
7
6
|
const url = `https://nsfw.xxx/page/${offset}?nsfw[]=0&types[]=image&types[]=video&types[]=gallery&slider=1&jsload=1&user=${user}&_=${Date.now()}`;
|
|
@@ -26,9 +25,9 @@ async function getUserPosts(user: string): Promise<string[]> {
|
|
|
26
25
|
return posts;
|
|
27
26
|
}
|
|
28
27
|
|
|
29
|
-
async function getPostsData(posts: string[]
|
|
28
|
+
async function getPostsData(posts: string[]): Promise<CoomerFileList> {
|
|
30
29
|
console.log('Fetching posts data...');
|
|
31
|
-
const
|
|
30
|
+
const filelist = new CoomerFileList();
|
|
32
31
|
for (const post of posts) {
|
|
33
32
|
const page = await fetch(post).then((r) => r.text());
|
|
34
33
|
const $ = cheerio.load(page);
|
|
@@ -46,16 +45,16 @@ async function getPostsData(posts: string[], mediaType: MediaType): Promise<File
|
|
|
46
45
|
const ext = src.split('.').pop();
|
|
47
46
|
const name = `${slug}-${date}.${ext}`;
|
|
48
47
|
|
|
49
|
-
|
|
48
|
+
filelist.files.push(CoomerFile.from({ name, url: src }));
|
|
50
49
|
}
|
|
51
50
|
|
|
52
|
-
return
|
|
51
|
+
return filelist;
|
|
53
52
|
}
|
|
54
53
|
|
|
55
|
-
export async function getRedditData(url: string
|
|
54
|
+
export async function getRedditData(url: string): Promise<CoomerFileList> {
|
|
56
55
|
const user = url.match(/u\/(\w+)/)?.[1] as string;
|
|
57
56
|
const posts = await getUserPosts(user);
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
return
|
|
57
|
+
const filelist = await getPostsData(posts);
|
|
58
|
+
filelist.dirName = `${user}-reddit`;
|
|
59
|
+
return filelist;
|
|
61
60
|
}
|
package/src/api/plain-curl.ts
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { CoomerFile, CoomerFileList } from '../services/file';
|
|
2
2
|
|
|
3
|
-
export async function getPlainFileData(url: string): Promise<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
url,
|
|
10
|
-
},
|
|
11
|
-
],
|
|
12
|
-
};
|
|
3
|
+
export async function getPlainFileData(url: string): Promise<CoomerFileList> {
|
|
4
|
+
const name = url.split('/').pop() as string;
|
|
5
|
+
const file = CoomerFile.from({ name, url });
|
|
6
|
+
const filelist = new CoomerFileList([file]);
|
|
7
|
+
filelist.dirName = '';
|
|
8
|
+
return filelist;
|
|
13
9
|
}
|
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import yargs from 'yargs';
|
|
2
2
|
import { hideBin } from 'yargs/helpers';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
[x: string]: unknown;
|
|
4
|
+
type ArgumentHandlerResult = {
|
|
6
5
|
url: string;
|
|
7
6
|
dir: string;
|
|
8
7
|
media: string;
|
|
9
8
|
include: string;
|
|
10
9
|
exclude: string;
|
|
11
10
|
skip: number;
|
|
12
|
-
_: (string | number)[];
|
|
13
|
-
$0: string;
|
|
14
11
|
};
|
|
15
12
|
|
|
16
13
|
export function argumentHander(): ArgumentHandlerResult {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Box } from 'ink';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { CoomerFileList } from '../../services/file';
|
|
4
|
+
import { FileBox, FileListStateBox, KeyboardControlsInfo, Loading, TitleBar } from './components';
|
|
5
|
+
import { useDownloaderHook } from './hooks/downloader';
|
|
6
|
+
import { useInputHook } from './hooks/input';
|
|
7
|
+
import { useInkStore } from './store';
|
|
8
|
+
|
|
9
|
+
export function App() {
|
|
10
|
+
useInputHook();
|
|
11
|
+
useDownloaderHook();
|
|
12
|
+
|
|
13
|
+
const downloader = useInkStore((state) => state.downloader);
|
|
14
|
+
const filelist = downloader?.filelist;
|
|
15
|
+
const isFilelist = filelist instanceof CoomerFileList;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Box borderStyle="single" flexDirection="column" borderColor="blue" width={80}>
|
|
19
|
+
<TitleBar />
|
|
20
|
+
{!isFilelist ? (
|
|
21
|
+
<Loading />
|
|
22
|
+
) : (
|
|
23
|
+
<>
|
|
24
|
+
<Box>
|
|
25
|
+
<Box>
|
|
26
|
+
<FileListStateBox filelist={filelist} />
|
|
27
|
+
</Box>
|
|
28
|
+
<Box flexBasis={29}>
|
|
29
|
+
<KeyboardControlsInfo />
|
|
30
|
+
</Box>
|
|
31
|
+
</Box>
|
|
32
|
+
|
|
33
|
+
{filelist.getActiveFiles().map((file) => {
|
|
34
|
+
return <FileBox file={file} key={file.name} />;
|
|
35
|
+
})}
|
|
36
|
+
</>
|
|
37
|
+
)}
|
|
38
|
+
</Box>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Box, Spacer, Text } from 'ink';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import type { CoomerFile } from '../../../services/file';
|
|
4
|
+
import { b2mb } from '../../../utils/strings';
|
|
5
|
+
import { Preview } from './preview';
|
|
6
|
+
import { Spinner } from './spinner';
|
|
7
|
+
|
|
8
|
+
interface FileBoxProps {
|
|
9
|
+
file: CoomerFile;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function FileBox({ file }: FileBoxProps) {
|
|
13
|
+
const percentage = Number((file.downloaded / (file.size as number)) * 100).toFixed(2);
|
|
14
|
+
return (
|
|
15
|
+
<>
|
|
16
|
+
<Box
|
|
17
|
+
borderStyle="single"
|
|
18
|
+
borderColor="magentaBright"
|
|
19
|
+
borderDimColor
|
|
20
|
+
paddingX={1}
|
|
21
|
+
flexDirection="column"
|
|
22
|
+
>
|
|
23
|
+
<Box>
|
|
24
|
+
<Text color="blue" dimColor wrap="truncate-middle">
|
|
25
|
+
{file.name}
|
|
26
|
+
</Text>
|
|
27
|
+
</Box>
|
|
28
|
+
<Box flexDirection="row-reverse">
|
|
29
|
+
<Text color="cyan" dimColor>
|
|
30
|
+
{b2mb(file.downloaded)}/{file.size ? b2mb(file.size) : '∞'} MB
|
|
31
|
+
</Text>
|
|
32
|
+
<Text color="redBright" dimColor>
|
|
33
|
+
{file.size ? ` ${percentage}% ` : ''}
|
|
34
|
+
</Text>
|
|
35
|
+
<Spacer />
|
|
36
|
+
<Text color={'green'} dimColor>
|
|
37
|
+
<Spinner></Spinner>
|
|
38
|
+
</Text>
|
|
39
|
+
</Box>
|
|
40
|
+
</Box>
|
|
41
|
+
<Preview file={file} />
|
|
42
|
+
</>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Box, Text } from 'ink';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import type { CoomerFileList } from '../../../services/file';
|
|
4
|
+
|
|
5
|
+
interface FileListStateBoxProps {
|
|
6
|
+
filelist: CoomerFileList;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function FileListStateBox({ filelist }: FileListStateBoxProps) {
|
|
10
|
+
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>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Box, Text } from 'ink';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
const info = {
|
|
5
|
+
's ': 'skip current file',
|
|
6
|
+
p: 'on/off image preview',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function KeyboardControlsInfo() {
|
|
10
|
+
const infoRender = Object.entries(info).map(([key, value]) => {
|
|
11
|
+
return (
|
|
12
|
+
<Box key={key}>
|
|
13
|
+
<Box marginRight={2}>
|
|
14
|
+
<Text color={'red'} dimColor bold>
|
|
15
|
+
{key}
|
|
16
|
+
</Text>
|
|
17
|
+
</Box>
|
|
18
|
+
<Text dimColor bold={false}>
|
|
19
|
+
{value}
|
|
20
|
+
</Text>
|
|
21
|
+
</Box>
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Box
|
|
27
|
+
flexDirection="column"
|
|
28
|
+
paddingX={1}
|
|
29
|
+
borderStyle={'single'}
|
|
30
|
+
borderColor={'gray'}
|
|
31
|
+
borderDimColor
|
|
32
|
+
>
|
|
33
|
+
<Box>
|
|
34
|
+
<Text color={'red'} dimColor bold>
|
|
35
|
+
Keyboard controls:
|
|
36
|
+
</Text>
|
|
37
|
+
</Box>
|
|
38
|
+
{infoRender}
|
|
39
|
+
</Box>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Box, Text } from 'ink';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Spinner } from './spinner';
|
|
4
|
+
|
|
5
|
+
export function Loading() {
|
|
6
|
+
return (
|
|
7
|
+
<Box paddingX={1} borderDimColor flexDirection="column">
|
|
8
|
+
<Box alignSelf="center">
|
|
9
|
+
<Text dimColor color={'redBright'}>
|
|
10
|
+
Fetching Data
|
|
11
|
+
</Text>
|
|
12
|
+
</Box>
|
|
13
|
+
<Box alignSelf="center">
|
|
14
|
+
<Text color={'blueBright'} dimColor>
|
|
15
|
+
<Spinner type="grenade"></Spinner>
|
|
16
|
+
</Text>
|
|
17
|
+
</Box>
|
|
18
|
+
</Box>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Box } from 'ink';
|
|
2
|
+
import Image, { TerminalInfoProvider } from 'ink-picture';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import type { CoomerFile } from '../../../services/file';
|
|
5
|
+
import { isImage } from '../../../utils/filters';
|
|
6
|
+
import { useInkStore } from '../store';
|
|
7
|
+
|
|
8
|
+
interface PreviewProps {
|
|
9
|
+
file: CoomerFile;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function Preview({ file }: PreviewProps) {
|
|
13
|
+
const previewEnabled = useInkStore((state) => state.preview);
|
|
14
|
+
const bigEnough = file.downloaded > 50 * 1024;
|
|
15
|
+
const shouldShow = previewEnabled && bigEnough && isImage(file.filepath as string);
|
|
16
|
+
const imgInfo = `
|
|
17
|
+
can't read partial images yet...
|
|
18
|
+
actual size: ${file.size}}
|
|
19
|
+
downloaded: ${file.downloaded}}
|
|
20
|
+
`;
|
|
21
|
+
return (
|
|
22
|
+
shouldShow && (
|
|
23
|
+
<Box paddingX={1}>
|
|
24
|
+
<TerminalInfoProvider>
|
|
25
|
+
<Box width={30} height={15}>
|
|
26
|
+
<Image src={file.filepath as string} alt={imgInfo} />
|
|
27
|
+
</Box>
|
|
28
|
+
</TerminalInfoProvider>
|
|
29
|
+
</Box>
|
|
30
|
+
)
|
|
31
|
+
);
|
|
32
|
+
}
|