mirror of
https://git.v0l.io/Kieran/dtan.git
synced 2024-12-12 15:06:22 +00:00
init
This commit is contained in:
commit
d2b8484697
11
.eslintrc.cjs
Normal file
11
.eslintrc.cjs
Normal file
@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react-hooks/recommended"],
|
||||
ignorePatterns: ["dist", ".eslintrc.cjs"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: ["react-refresh"],
|
||||
rules: {
|
||||
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
||||
},
|
||||
};
|
32
.gitignore
vendored
Normal file
32
.gitignore
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["arcanis.vscode-zipfs", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
|
||||
}
|
893
.yarn/releases/yarn-4.0.2.cjs
vendored
Executable file
893
.yarn/releases/yarn-4.0.2.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
20
.yarn/sdks/eslint/bin/eslint.js
vendored
Executable file
20
.yarn/sdks/eslint/bin/eslint.js
vendored
Executable file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { existsSync } = require(`fs`);
|
||||
const { createRequire } = require(`module`);
|
||||
const { resolve } = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require eslint/bin/eslint.js
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real eslint/bin/eslint.js your application uses
|
||||
module.exports = absRequire(`eslint/bin/eslint.js`);
|
20
.yarn/sdks/eslint/lib/api.js
vendored
Normal file
20
.yarn/sdks/eslint/lib/api.js
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { existsSync } = require(`fs`);
|
||||
const { createRequire } = require(`module`);
|
||||
const { resolve } = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require eslint
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real eslint your application uses
|
||||
module.exports = absRequire(`eslint`);
|
20
.yarn/sdks/eslint/lib/unsupported-api.js
vendored
Normal file
20
.yarn/sdks/eslint/lib/unsupported-api.js
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { existsSync } = require(`fs`);
|
||||
const { createRequire } = require(`module`);
|
||||
const { resolve } = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require eslint/use-at-your-own-risk
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real eslint/use-at-your-own-risk your application uses
|
||||
module.exports = absRequire(`eslint/use-at-your-own-risk`);
|
14
.yarn/sdks/eslint/package.json
vendored
Normal file
14
.yarn/sdks/eslint/package.json
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "eslint",
|
||||
"version": "8.54.0-sdk",
|
||||
"main": "./lib/api.js",
|
||||
"type": "commonjs",
|
||||
"bin": {
|
||||
"eslint": "./bin/eslint.js"
|
||||
},
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": "./lib/api.js",
|
||||
"./use-at-your-own-risk": "./lib/unsupported-api.js"
|
||||
}
|
||||
}
|
5
.yarn/sdks/integrations.yml
vendored
Normal file
5
.yarn/sdks/integrations.yml
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
# This file is automatically generated by @yarnpkg/sdks.
|
||||
# Manual changes might be lost!
|
||||
|
||||
integrations:
|
||||
- vscode
|
20
.yarn/sdks/prettier/bin/prettier.cjs
vendored
Executable file
20
.yarn/sdks/prettier/bin/prettier.cjs
vendored
Executable file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { existsSync } = require(`fs`);
|
||||
const { createRequire } = require(`module`);
|
||||
const { resolve } = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require prettier/bin/prettier.cjs
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real prettier/bin/prettier.cjs your application uses
|
||||
module.exports = absRequire(`prettier/bin/prettier.cjs`);
|
20
.yarn/sdks/prettier/index.cjs
vendored
Normal file
20
.yarn/sdks/prettier/index.cjs
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { existsSync } = require(`fs`);
|
||||
const { createRequire } = require(`module`);
|
||||
const { resolve } = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require prettier
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real prettier your application uses
|
||||
module.exports = absRequire(`prettier`);
|
7
.yarn/sdks/prettier/package.json
vendored
Normal file
7
.yarn/sdks/prettier/package.json
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "prettier",
|
||||
"version": "3.1.0-sdk",
|
||||
"main": "./index.cjs",
|
||||
"type": "commonjs",
|
||||
"bin": "./bin/prettier.cjs"
|
||||
}
|
20
.yarn/sdks/typescript/bin/tsc
vendored
Executable file
20
.yarn/sdks/typescript/bin/tsc
vendored
Executable file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { existsSync } = require(`fs`);
|
||||
const { createRequire } = require(`module`);
|
||||
const { resolve } = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require typescript/bin/tsc
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real typescript/bin/tsc your application uses
|
||||
module.exports = absRequire(`typescript/bin/tsc`);
|
20
.yarn/sdks/typescript/bin/tsserver
vendored
Executable file
20
.yarn/sdks/typescript/bin/tsserver
vendored
Executable file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { existsSync } = require(`fs`);
|
||||
const { createRequire } = require(`module`);
|
||||
const { resolve } = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require typescript/bin/tsserver
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real typescript/bin/tsserver your application uses
|
||||
module.exports = absRequire(`typescript/bin/tsserver`);
|
20
.yarn/sdks/typescript/lib/tsc.js
vendored
Normal file
20
.yarn/sdks/typescript/lib/tsc.js
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { existsSync } = require(`fs`);
|
||||
const { createRequire } = require(`module`);
|
||||
const { resolve } = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require typescript/lib/tsc.js
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real typescript/lib/tsc.js your application uses
|
||||
module.exports = absRequire(`typescript/lib/tsc.js`);
|
252
.yarn/sdks/typescript/lib/tsserver.js
vendored
Normal file
252
.yarn/sdks/typescript/lib/tsserver.js
vendored
Normal file
@ -0,0 +1,252 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { existsSync } = require(`fs`);
|
||||
const { createRequire } = require(`module`);
|
||||
const { resolve } = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
const moduleWrapper = (tsserver) => {
|
||||
if (!process.versions.pnp) {
|
||||
return tsserver;
|
||||
}
|
||||
|
||||
const { isAbsolute } = require(`path`);
|
||||
const pnpApi = require(`pnpapi`);
|
||||
|
||||
const isVirtual = (str) => str.match(/\/(\$\$virtual|__virtual__)\//);
|
||||
const isPortal = (str) => str.startsWith("portal:/");
|
||||
const normalize = (str) => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
|
||||
|
||||
const dependencyTreeRoots = new Set(
|
||||
pnpApi.getDependencyTreeRoots().map((locator) => {
|
||||
return `${locator.name}@${locator.reference}`;
|
||||
}),
|
||||
);
|
||||
|
||||
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
|
||||
// doesn't understand. This layer makes sure to remove the protocol
|
||||
// before forwarding it to TS, and to add it back on all returned paths.
|
||||
|
||||
function toEditorPath(str) {
|
||||
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
|
||||
if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) {
|
||||
// We also take the opportunity to turn virtual paths into physical ones;
|
||||
// this makes it much easier to work with workspaces that list peer
|
||||
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
|
||||
// file instances instead of the real ones.
|
||||
//
|
||||
// We only do this to modules owned by the the dependency tree roots.
|
||||
// This avoids breaking the resolution when jumping inside a vendor
|
||||
// with peer dep (otherwise jumping into react-dom would show resolution
|
||||
// errors on react).
|
||||
//
|
||||
const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str;
|
||||
if (resolved) {
|
||||
const locator = pnpApi.findPackageLocator(resolved);
|
||||
if (
|
||||
locator &&
|
||||
(dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))
|
||||
) {
|
||||
str = resolved;
|
||||
}
|
||||
}
|
||||
|
||||
str = normalize(str);
|
||||
|
||||
if (str.match(/\.zip\//)) {
|
||||
switch (hostInfo) {
|
||||
// Absolute VSCode `Uri.fsPath`s need to start with a slash.
|
||||
// VSCode only adds it automatically for supported schemes,
|
||||
// so we have to do it manually for the `zip` scheme.
|
||||
// The path needs to start with a caret otherwise VSCode doesn't handle the protocol
|
||||
//
|
||||
// Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910
|
||||
//
|
||||
// 2021-10-08: VSCode changed the format in 1.61.
|
||||
// Before | ^zip:/c:/foo/bar.zip/package.json
|
||||
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||
//
|
||||
// 2022-04-06: VSCode changed the format in 1.66.
|
||||
// Before | ^/zip//c:/foo/bar.zip/package.json
|
||||
// After | ^/zip/c:/foo/bar.zip/package.json
|
||||
//
|
||||
// 2022-05-06: VSCode changed the format in 1.68
|
||||
// Before | ^/zip/c:/foo/bar.zip/package.json
|
||||
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||
//
|
||||
case `vscode <1.61`:
|
||||
{
|
||||
str = `^zip:${str}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case `vscode <1.66`:
|
||||
{
|
||||
str = `^/zip/${str}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case `vscode <1.68`:
|
||||
{
|
||||
str = `^/zip${str}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case `vscode`:
|
||||
{
|
||||
str = `^/zip/${str}`;
|
||||
}
|
||||
break;
|
||||
|
||||
// To make "go to definition" work,
|
||||
// We have to resolve the actual file system path from virtual path
|
||||
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
|
||||
case `coc-nvim`:
|
||||
{
|
||||
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||
str = resolve(`zipfile:${str}`);
|
||||
}
|
||||
break;
|
||||
|
||||
// Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server)
|
||||
// We have to resolve the actual file system path from virtual path,
|
||||
// everything else is up to neovim
|
||||
case `neovim`:
|
||||
{
|
||||
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||
str = `zipfile://${str}`;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
{
|
||||
str = `zip:${str}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`);
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
function fromEditorPath(str) {
|
||||
switch (hostInfo) {
|
||||
case `coc-nvim`:
|
||||
{
|
||||
str = str.replace(/\.zip::/, `.zip/`);
|
||||
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
|
||||
// So in order to convert it back, we use .* to match all the thing
|
||||
// before `zipfile:`
|
||||
return process.platform === `win32` ? str.replace(/^.*zipfile:\//, ``) : str.replace(/^.*zipfile:/, ``);
|
||||
}
|
||||
break;
|
||||
|
||||
case `neovim`:
|
||||
{
|
||||
str = str.replace(/\.zip::/, `.zip/`);
|
||||
// The path for neovim is in format of zipfile:///<pwd>/.yarn/...
|
||||
return str.replace(/^zipfile:\/\//, ``);
|
||||
}
|
||||
break;
|
||||
|
||||
case `vscode`:
|
||||
default:
|
||||
{
|
||||
return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Force enable 'allowLocalPluginLoads'
|
||||
// TypeScript tries to resolve plugins using a path relative to itself
|
||||
// which doesn't work when using the global cache
|
||||
// https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238
|
||||
// VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but
|
||||
// TypeScript already does local loads and if this code is running the user trusts the workspace
|
||||
// https://github.com/microsoft/vscode/issues/45856
|
||||
const ConfiguredProject = tsserver.server.ConfiguredProject;
|
||||
const { enablePluginsWithOptions: originalEnablePluginsWithOptions } = ConfiguredProject.prototype;
|
||||
ConfiguredProject.prototype.enablePluginsWithOptions = function () {
|
||||
this.projectService.allowLocalPluginLoads = true;
|
||||
return originalEnablePluginsWithOptions.apply(this, arguments);
|
||||
};
|
||||
|
||||
// And here is the point where we hijack the VSCode <-> TS communications
|
||||
// by adding ourselves in the middle. We locate everything that looks
|
||||
// like an absolute path of ours and normalize it.
|
||||
|
||||
const Session = tsserver.server.Session;
|
||||
const { onMessage: originalOnMessage, send: originalSend } = Session.prototype;
|
||||
let hostInfo = `unknown`;
|
||||
|
||||
Object.assign(Session.prototype, {
|
||||
onMessage(/** @type {string | object} */ message) {
|
||||
const isStringMessage = typeof message === "string";
|
||||
const parsedMessage = isStringMessage ? JSON.parse(message) : message;
|
||||
|
||||
if (
|
||||
parsedMessage != null &&
|
||||
typeof parsedMessage === `object` &&
|
||||
parsedMessage.arguments &&
|
||||
typeof parsedMessage.arguments.hostInfo === `string`
|
||||
) {
|
||||
hostInfo = parsedMessage.arguments.hostInfo;
|
||||
if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) {
|
||||
const [, major, minor] = (
|
||||
process.env.VSCODE_IPC_HOOK.match(
|
||||
// The RegExp from https://semver.org/ but without the caret at the start
|
||||
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/,
|
||||
) ?? []
|
||||
).map(Number);
|
||||
|
||||
if (major === 1) {
|
||||
if (minor < 61) {
|
||||
hostInfo += ` <1.61`;
|
||||
} else if (minor < 66) {
|
||||
hostInfo += ` <1.66`;
|
||||
} else if (minor < 68) {
|
||||
hostInfo += ` <1.68`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => {
|
||||
return typeof value === "string" ? fromEditorPath(value) : value;
|
||||
});
|
||||
|
||||
return originalOnMessage.call(this, isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON));
|
||||
},
|
||||
|
||||
send(/** @type {any} */ msg) {
|
||||
return originalSend.call(
|
||||
this,
|
||||
JSON.parse(
|
||||
JSON.stringify(msg, (key, value) => {
|
||||
return typeof value === `string` ? toEditorPath(value) : value;
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return tsserver;
|
||||
};
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require typescript/lib/tsserver.js
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real typescript/lib/tsserver.js your application uses
|
||||
module.exports = moduleWrapper(absRequire(`typescript/lib/tsserver.js`));
|
252
.yarn/sdks/typescript/lib/tsserverlibrary.js
vendored
Normal file
252
.yarn/sdks/typescript/lib/tsserverlibrary.js
vendored
Normal file
@ -0,0 +1,252 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { existsSync } = require(`fs`);
|
||||
const { createRequire } = require(`module`);
|
||||
const { resolve } = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
const moduleWrapper = (tsserver) => {
|
||||
if (!process.versions.pnp) {
|
||||
return tsserver;
|
||||
}
|
||||
|
||||
const { isAbsolute } = require(`path`);
|
||||
const pnpApi = require(`pnpapi`);
|
||||
|
||||
const isVirtual = (str) => str.match(/\/(\$\$virtual|__virtual__)\//);
|
||||
const isPortal = (str) => str.startsWith("portal:/");
|
||||
const normalize = (str) => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
|
||||
|
||||
const dependencyTreeRoots = new Set(
|
||||
pnpApi.getDependencyTreeRoots().map((locator) => {
|
||||
return `${locator.name}@${locator.reference}`;
|
||||
}),
|
||||
);
|
||||
|
||||
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
|
||||
// doesn't understand. This layer makes sure to remove the protocol
|
||||
// before forwarding it to TS, and to add it back on all returned paths.
|
||||
|
||||
function toEditorPath(str) {
|
||||
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
|
||||
if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) {
|
||||
// We also take the opportunity to turn virtual paths into physical ones;
|
||||
// this makes it much easier to work with workspaces that list peer
|
||||
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
|
||||
// file instances instead of the real ones.
|
||||
//
|
||||
// We only do this to modules owned by the the dependency tree roots.
|
||||
// This avoids breaking the resolution when jumping inside a vendor
|
||||
// with peer dep (otherwise jumping into react-dom would show resolution
|
||||
// errors on react).
|
||||
//
|
||||
const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str;
|
||||
if (resolved) {
|
||||
const locator = pnpApi.findPackageLocator(resolved);
|
||||
if (
|
||||
locator &&
|
||||
(dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))
|
||||
) {
|
||||
str = resolved;
|
||||
}
|
||||
}
|
||||
|
||||
str = normalize(str);
|
||||
|
||||
if (str.match(/\.zip\//)) {
|
||||
switch (hostInfo) {
|
||||
// Absolute VSCode `Uri.fsPath`s need to start with a slash.
|
||||
// VSCode only adds it automatically for supported schemes,
|
||||
// so we have to do it manually for the `zip` scheme.
|
||||
// The path needs to start with a caret otherwise VSCode doesn't handle the protocol
|
||||
//
|
||||
// Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910
|
||||
//
|
||||
// 2021-10-08: VSCode changed the format in 1.61.
|
||||
// Before | ^zip:/c:/foo/bar.zip/package.json
|
||||
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||
//
|
||||
// 2022-04-06: VSCode changed the format in 1.66.
|
||||
// Before | ^/zip//c:/foo/bar.zip/package.json
|
||||
// After | ^/zip/c:/foo/bar.zip/package.json
|
||||
//
|
||||
// 2022-05-06: VSCode changed the format in 1.68
|
||||
// Before | ^/zip/c:/foo/bar.zip/package.json
|
||||
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||
//
|
||||
case `vscode <1.61`:
|
||||
{
|
||||
str = `^zip:${str}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case `vscode <1.66`:
|
||||
{
|
||||
str = `^/zip/${str}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case `vscode <1.68`:
|
||||
{
|
||||
str = `^/zip${str}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case `vscode`:
|
||||
{
|
||||
str = `^/zip/${str}`;
|
||||
}
|
||||
break;
|
||||
|
||||
// To make "go to definition" work,
|
||||
// We have to resolve the actual file system path from virtual path
|
||||
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
|
||||
case `coc-nvim`:
|
||||
{
|
||||
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||
str = resolve(`zipfile:${str}`);
|
||||
}
|
||||
break;
|
||||
|
||||
// Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server)
|
||||
// We have to resolve the actual file system path from virtual path,
|
||||
// everything else is up to neovim
|
||||
case `neovim`:
|
||||
{
|
||||
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||
str = `zipfile://${str}`;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
{
|
||||
str = `zip:${str}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`);
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
function fromEditorPath(str) {
|
||||
switch (hostInfo) {
|
||||
case `coc-nvim`:
|
||||
{
|
||||
str = str.replace(/\.zip::/, `.zip/`);
|
||||
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
|
||||
// So in order to convert it back, we use .* to match all the thing
|
||||
// before `zipfile:`
|
||||
return process.platform === `win32` ? str.replace(/^.*zipfile:\//, ``) : str.replace(/^.*zipfile:/, ``);
|
||||
}
|
||||
break;
|
||||
|
||||
case `neovim`:
|
||||
{
|
||||
str = str.replace(/\.zip::/, `.zip/`);
|
||||
// The path for neovim is in format of zipfile:///<pwd>/.yarn/...
|
||||
return str.replace(/^zipfile:\/\//, ``);
|
||||
}
|
||||
break;
|
||||
|
||||
case `vscode`:
|
||||
default:
|
||||
{
|
||||
return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Force enable 'allowLocalPluginLoads'
|
||||
// TypeScript tries to resolve plugins using a path relative to itself
|
||||
// which doesn't work when using the global cache
|
||||
// https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238
|
||||
// VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but
|
||||
// TypeScript already does local loads and if this code is running the user trusts the workspace
|
||||
// https://github.com/microsoft/vscode/issues/45856
|
||||
const ConfiguredProject = tsserver.server.ConfiguredProject;
|
||||
const { enablePluginsWithOptions: originalEnablePluginsWithOptions } = ConfiguredProject.prototype;
|
||||
ConfiguredProject.prototype.enablePluginsWithOptions = function () {
|
||||
this.projectService.allowLocalPluginLoads = true;
|
||||
return originalEnablePluginsWithOptions.apply(this, arguments);
|
||||
};
|
||||
|
||||
// And here is the point where we hijack the VSCode <-> TS communications
|
||||
// by adding ourselves in the middle. We locate everything that looks
|
||||
// like an absolute path of ours and normalize it.
|
||||
|
||||
const Session = tsserver.server.Session;
|
||||
const { onMessage: originalOnMessage, send: originalSend } = Session.prototype;
|
||||
let hostInfo = `unknown`;
|
||||
|
||||
Object.assign(Session.prototype, {
|
||||
onMessage(/** @type {string | object} */ message) {
|
||||
const isStringMessage = typeof message === "string";
|
||||
const parsedMessage = isStringMessage ? JSON.parse(message) : message;
|
||||
|
||||
if (
|
||||
parsedMessage != null &&
|
||||
typeof parsedMessage === `object` &&
|
||||
parsedMessage.arguments &&
|
||||
typeof parsedMessage.arguments.hostInfo === `string`
|
||||
) {
|
||||
hostInfo = parsedMessage.arguments.hostInfo;
|
||||
if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) {
|
||||
const [, major, minor] = (
|
||||
process.env.VSCODE_IPC_HOOK.match(
|
||||
// The RegExp from https://semver.org/ but without the caret at the start
|
||||
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/,
|
||||
) ?? []
|
||||
).map(Number);
|
||||
|
||||
if (major === 1) {
|
||||
if (minor < 61) {
|
||||
hostInfo += ` <1.61`;
|
||||
} else if (minor < 66) {
|
||||
hostInfo += ` <1.66`;
|
||||
} else if (minor < 68) {
|
||||
hostInfo += ` <1.68`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => {
|
||||
return typeof value === "string" ? fromEditorPath(value) : value;
|
||||
});
|
||||
|
||||
return originalOnMessage.call(this, isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON));
|
||||
},
|
||||
|
||||
send(/** @type {any} */ msg) {
|
||||
return originalSend.call(
|
||||
this,
|
||||
JSON.parse(
|
||||
JSON.stringify(msg, (key, value) => {
|
||||
return typeof value === `string` ? toEditorPath(value) : value;
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return tsserver;
|
||||
};
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require typescript/lib/tsserverlibrary.js
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real typescript/lib/tsserverlibrary.js your application uses
|
||||
module.exports = moduleWrapper(absRequire(`typescript/lib/tsserverlibrary.js`));
|
20
.yarn/sdks/typescript/lib/typescript.js
vendored
Normal file
20
.yarn/sdks/typescript/lib/typescript.js
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { existsSync } = require(`fs`);
|
||||
const { createRequire } = require(`module`);
|
||||
const { resolve } = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require typescript
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real typescript your application uses
|
||||
module.exports = absRequire(`typescript`);
|
10
.yarn/sdks/typescript/package.json
vendored
Normal file
10
.yarn/sdks/typescript/package.json
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "typescript",
|
||||
"version": "5.3.2-sdk",
|
||||
"main": "./lib/typescript.js",
|
||||
"type": "commonjs",
|
||||
"bin": {
|
||||
"tsc": "./bin/tsc",
|
||||
"tsserver": "./bin/tsserver"
|
||||
}
|
||||
}
|
2
.yarnrc.yml
Normal file
2
.yarnrc.yml
Normal file
@ -0,0 +1,2 @@
|
||||
nodeLinker: pnp
|
||||
yarnPath: .yarn/releases/yarn-4.0.2.cjs
|
22
README.md
Normal file
22
README.md
Normal file
@ -0,0 +1,22 @@
|
||||
```json
|
||||
{
|
||||
"kind": 2003,›
|
||||
"content": "<long-description-pre-formatted>",
|
||||
"tags": [
|
||||
["title", "<torrent-title>"],
|
||||
["size", "<size-in-bytes>"],
|
||||
["btih", "<bittorrent-info-hash>"],
|
||||
["t", "<top-level-tag>"],
|
||||
["t", "(optional)<second-level-tag>"],
|
||||
["t", ...other tags],
|
||||
["file", "<file-name>", "<file-size-in-bytes>"],
|
||||
["file", "<file-name>", "<file-size-in-bytes>"],
|
||||
["file", ...other files],
|
||||
["imdb", "(optional)<imdb-entry-id>"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Top Level tags are usually the type of content like `"Video"` / `"Audio"` / `"Application"` and so on.
|
||||
|
||||
Second level tags are sub categories like `"Movies"` / `"CAD/CAM"`
|
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>dtan.xyz</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
43
package.json
Normal file
43
package.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "dtan",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@noble/hashes": "^1.3.2",
|
||||
"@snort/shared": "^1.0.10",
|
||||
"@snort/system": "^1.1.5",
|
||||
"@snort/system-react": "^1.1.5",
|
||||
"@snort/system-web": "^1.0.3",
|
||||
"classnames": "^2.3.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.37",
|
||||
"@types/react-dom": "^18.2.15",
|
||||
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
||||
"@typescript-eslint/parser": "^6.10.0",
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"eslint": "^8.53.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.4",
|
||||
"postcss": "^8.4.31",
|
||||
"prettier": "^3.1.0",
|
||||
"tailwindcss": "^3.3.5",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.0.0"
|
||||
},
|
||||
"packageManager": "yarn@4.0.2",
|
||||
"prettier": {
|
||||
"printWidth": 120
|
||||
}
|
||||
}
|
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
158
src/bencode/decode.ts
Normal file
158
src/bencode/decode.ts
Normal file
@ -0,0 +1,158 @@
|
||||
import { bytesToHex } from "@noble/hashes/utils";
|
||||
|
||||
const INTEGER_START = 0x69; // 'i'
|
||||
const STRING_DELIM = 0x3a; // ':'
|
||||
const DICTIONARY_START = 0x64; // 'd'
|
||||
const LIST_START = 0x6c; // 'l'
|
||||
const END_OF_TYPE = 0x65; // 'e'
|
||||
|
||||
export type BencodeValue = number | Uint8Array | BencodeValue[] | { [key: string]: BencodeValue };
|
||||
|
||||
/**
|
||||
* replaces parseInt(buffer.toString('ascii', start, end)).
|
||||
* For strings with less then ~30 charachters, this is actually a lot faster.
|
||||
*
|
||||
* @param {Uint8Array} buffer
|
||||
* @param {Number} start
|
||||
* @param {Number} end
|
||||
* @return {Number} calculated number
|
||||
*/
|
||||
function getIntFromBuffer(buffer: Uint8Array, start: number, end: number) {
|
||||
let sum = 0;
|
||||
let sign = 1;
|
||||
|
||||
for (let i = start; i < end; i++) {
|
||||
const num = buffer[i];
|
||||
|
||||
if (num < 58 && num >= 48) {
|
||||
sum = sum * 10 + (num - 48);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i === start && num === 43) {
|
||||
// +
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i === start && num === 45) {
|
||||
// -
|
||||
sign = -1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (num === 46) {
|
||||
// .
|
||||
// its a float. break here.
|
||||
break;
|
||||
}
|
||||
|
||||
throw new Error("not a number: buffer[" + i + "] = " + num);
|
||||
}
|
||||
|
||||
return sum * sign;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes bencoded data.
|
||||
*
|
||||
* @param {Uint8Array} data
|
||||
* @param {Number} start (optional)
|
||||
* @param {Number} end (optional)
|
||||
* @param {String} encoding (optional)
|
||||
* @return {Object|Array|Uint8Array|String|Number}
|
||||
*/
|
||||
export function decode(data: Uint8Array, start?: number, end?: number, encoding?: string) {
|
||||
const dec = {
|
||||
position: 0,
|
||||
bytes: 0,
|
||||
encoding,
|
||||
data: data.subarray(start, end),
|
||||
} as Decode;
|
||||
dec.bytes = dec.data.length;
|
||||
return next(dec);
|
||||
}
|
||||
|
||||
interface Decode {
|
||||
bytes: number;
|
||||
position: number;
|
||||
data: Uint8Array;
|
||||
encoding?: string;
|
||||
}
|
||||
|
||||
function buffer(dec: Decode) {
|
||||
let sep = find(dec, STRING_DELIM);
|
||||
const length = getIntFromBuffer(dec.data, dec.position, sep);
|
||||
const end = ++sep + length;
|
||||
|
||||
dec.position = end;
|
||||
|
||||
return dec.data.subarray(sep, end);
|
||||
}
|
||||
|
||||
function next(dec: Decode): BencodeValue {
|
||||
switch (dec.data[dec.position]) {
|
||||
case DICTIONARY_START:
|
||||
return dictionary(dec);
|
||||
case LIST_START:
|
||||
return list(dec);
|
||||
case INTEGER_START:
|
||||
return integer(dec);
|
||||
default:
|
||||
return buffer(dec);
|
||||
}
|
||||
}
|
||||
|
||||
function find(dec: Decode, chr: number) {
|
||||
let i = dec.position;
|
||||
const c = dec.data.length;
|
||||
const d = dec.data;
|
||||
|
||||
while (i < c) {
|
||||
if (d[i] === chr) return i;
|
||||
i++;
|
||||
}
|
||||
|
||||
throw new Error('Invalid data: Missing delimiter "' + String.fromCharCode(chr) + '" [0x' + chr.toString(16) + "]");
|
||||
}
|
||||
|
||||
function dictionary(dec: Decode) {
|
||||
dec.position++;
|
||||
|
||||
const dict = {} as Record<string, BencodeValue>;
|
||||
|
||||
while (dec.data[dec.position] !== END_OF_TYPE) {
|
||||
const bf = buffer(dec);
|
||||
let key = new TextDecoder().decode(bf);
|
||||
if (key.includes("\uFFFD")) key = bytesToHex(bf);
|
||||
dict[key] = next(dec);
|
||||
}
|
||||
|
||||
dec.position++;
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
||||
function list(dec: Decode) {
|
||||
dec.position++;
|
||||
|
||||
const lst = [] as Array<BencodeValue>;
|
||||
|
||||
while (dec.data[dec.position] !== END_OF_TYPE) {
|
||||
lst.push(next(dec));
|
||||
}
|
||||
|
||||
dec.position++;
|
||||
|
||||
return lst;
|
||||
}
|
||||
|
||||
function integer(dec: Decode) {
|
||||
const end = find(dec, END_OF_TYPE);
|
||||
const number = getIntFromBuffer(dec.data, dec.position + 1, end);
|
||||
|
||||
dec.position += end + 1 - dec.position;
|
||||
|
||||
return number;
|
||||
}
|
||||
|
||||
export default decode;
|
184
src/bencode/encode.ts
Normal file
184
src/bencode/encode.ts
Normal file
@ -0,0 +1,184 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
function getType(value: any) {
|
||||
if (ArrayBuffer.isView(value)) return "arraybufferview";
|
||||
if (Array.isArray(value)) return "array";
|
||||
if (value instanceof Number) return "number";
|
||||
if (value instanceof Boolean) return "boolean";
|
||||
if (value instanceof Set) return "set";
|
||||
if (value instanceof Map) return "map";
|
||||
if (value instanceof String) return "string";
|
||||
if (value instanceof ArrayBuffer) return "arraybuffer";
|
||||
return typeof value;
|
||||
}
|
||||
|
||||
function text2arr(data: string) {
|
||||
return new TextEncoder().encode(data);
|
||||
}
|
||||
|
||||
function concat(arrays: Uint8Array[]): Uint8Array {
|
||||
// Calculate the total length of all arrays
|
||||
const totalLength = arrays.reduce((acc, value) => acc + value.length, 0);
|
||||
|
||||
// Create a new array with total length and fill it with elements of the arrays
|
||||
const result = new Uint8Array(totalLength);
|
||||
|
||||
// Copy each array into the result
|
||||
let length = 0;
|
||||
for (const array of arrays) {
|
||||
result.set(array, length);
|
||||
length += array.length;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes data in bencode.
|
||||
*
|
||||
* @param {Uint8Array|Array|String|Object|Number|Boolean} data
|
||||
* @return {Uint8Array}
|
||||
*/
|
||||
export function encode(data: any, outBuffer?: Uint8Array, offset?: number) {
|
||||
const buffers = [] as Array<Uint8Array>;
|
||||
let result = null;
|
||||
|
||||
encode._encode(buffers, data);
|
||||
result = concat(buffers);
|
||||
encode.bytes = result.length;
|
||||
|
||||
if (ArrayBuffer.isView(outBuffer)) {
|
||||
outBuffer.set(result, offset);
|
||||
return outBuffer;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
encode.bytes = -1;
|
||||
encode._floatConversionDetected = false;
|
||||
|
||||
encode._encode = function (buffers: Array<Uint8Array>, data: any) {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (getType(data)) {
|
||||
case "object":
|
||||
encode.dict(buffers, data);
|
||||
break;
|
||||
case "map":
|
||||
encode.dictMap(buffers, data);
|
||||
break;
|
||||
case "array":
|
||||
encode.list(buffers, data);
|
||||
break;
|
||||
case "set":
|
||||
encode.listSet(buffers, data);
|
||||
break;
|
||||
case "string":
|
||||
encode.string(buffers, data);
|
||||
break;
|
||||
case "number":
|
||||
encode.number(buffers, data);
|
||||
break;
|
||||
case "boolean":
|
||||
encode.number(buffers, data);
|
||||
break;
|
||||
case "arraybufferview":
|
||||
encode.buffer(buffers, new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
|
||||
break;
|
||||
case "arraybuffer":
|
||||
encode.buffer(buffers, new Uint8Array(data));
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const buffE = new Uint8Array([0x65]);
|
||||
const buffD = new Uint8Array([0x64]);
|
||||
const buffL = new Uint8Array([0x6c]);
|
||||
|
||||
encode.buffer = function (buffers: Array<Uint8Array>, data: any) {
|
||||
buffers.push(text2arr(data.length + ":"), data);
|
||||
};
|
||||
|
||||
encode.string = function (buffers: Array<Uint8Array>, data: any) {
|
||||
buffers.push(text2arr(text2arr(data).byteLength + ":" + data));
|
||||
};
|
||||
|
||||
encode.number = function (buffers: Array<Uint8Array>, data: any) {
|
||||
if (Number.isInteger(data)) return buffers.push(text2arr("i" + BigInt(data) + "e"));
|
||||
|
||||
const maxLo = 0x80000000;
|
||||
const hi = (data / maxLo) << 0;
|
||||
const lo = data % maxLo << 0;
|
||||
const val = hi * maxLo + lo;
|
||||
|
||||
buffers.push(text2arr("i" + val + "e"));
|
||||
|
||||
if (val !== data && !encode._floatConversionDetected) {
|
||||
encode._floatConversionDetected = true;
|
||||
console.warn(
|
||||
'WARNING: Possible data corruption detected with value "' + data + '":',
|
||||
'Bencoding only defines support for integers, value was converted to "' + val + '"',
|
||||
);
|
||||
console.trace();
|
||||
}
|
||||
};
|
||||
|
||||
encode.dict = function (buffers: Array<Uint8Array>, data: any) {
|
||||
buffers.push(buffD);
|
||||
|
||||
let j = 0;
|
||||
let k;
|
||||
// fix for issue #13 - sorted dicts
|
||||
const keys = Object.keys(data).sort();
|
||||
const kl = keys.length;
|
||||
|
||||
for (; j < kl; j++) {
|
||||
k = keys[j];
|
||||
if (data[k] == null) continue;
|
||||
encode.string(buffers, k);
|
||||
encode._encode(buffers, data[k]);
|
||||
}
|
||||
|
||||
buffers.push(buffE);
|
||||
};
|
||||
|
||||
encode.dictMap = function (buffers: Array<Uint8Array>, data: any) {
|
||||
buffers.push(buffD);
|
||||
|
||||
const keys = Array.from(data.keys()).sort();
|
||||
|
||||
for (const key of keys) {
|
||||
if (data.get(key) == null) continue;
|
||||
ArrayBuffer.isView(key) ? encode._encode(buffers, key) : encode.string(buffers, String(key));
|
||||
encode._encode(buffers, data.get(key));
|
||||
}
|
||||
|
||||
buffers.push(buffE);
|
||||
};
|
||||
|
||||
encode.list = function (buffers: Array<Uint8Array>, data: any) {
|
||||
let i = 0;
|
||||
const c = data.length;
|
||||
buffers.push(buffL);
|
||||
|
||||
for (; i < c; i++) {
|
||||
if (data[i] == null) continue;
|
||||
encode._encode(buffers, data[i]);
|
||||
}
|
||||
|
||||
buffers.push(buffE);
|
||||
};
|
||||
|
||||
encode.listSet = function (buffers: Array<Uint8Array>, data: any) {
|
||||
buffers.push(buffL);
|
||||
|
||||
for (const item of data) {
|
||||
if (item == null) continue;
|
||||
encode._encode(buffers, item);
|
||||
}
|
||||
|
||||
buffers.push(buffE);
|
||||
};
|
2
src/bencode/index.ts
Normal file
2
src/bencode/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./encode";
|
||||
export * from "./decode";
|
133
src/const.ts
Normal file
133
src/const.ts
Normal file
@ -0,0 +1,133 @@
|
||||
import { EventKind } from "@snort/system";
|
||||
|
||||
/**
|
||||
* @constant {number} - Size of 1 kiB
|
||||
*/
|
||||
export const kiB = Math.pow(1024, 1);
|
||||
/**
|
||||
* @constant {number} - Size of 1 MiB
|
||||
*/
|
||||
export const MiB = Math.pow(1024, 2);
|
||||
/**
|
||||
* @constant {number} - Size of 1 GiB
|
||||
*/
|
||||
export const GiB = Math.pow(1024, 3);
|
||||
/**
|
||||
* @constant {number} - Size of 1 TiB
|
||||
*/
|
||||
export const TiB = Math.pow(1024, 4);
|
||||
/**
|
||||
* @constant {number} - Size of 1 PiB
|
||||
*/
|
||||
export const PiB = Math.pow(1024, 5);
|
||||
/**
|
||||
* @constant {number} - Size of 1 EiB
|
||||
*/
|
||||
export const EiB = Math.pow(1024, 6);
|
||||
/**
|
||||
* @constant {number} - Size of 1 ZiB
|
||||
*/
|
||||
export const ZiB = Math.pow(1024, 7);
|
||||
/**
|
||||
* @constant {number} - Size of 1 YiB
|
||||
*/
|
||||
export const YiB = Math.pow(1024, 8);
|
||||
|
||||
export interface Category {
|
||||
name: string;
|
||||
tag: string;
|
||||
sub_category?: Array<Category>;
|
||||
}
|
||||
|
||||
export const Categories = [
|
||||
{
|
||||
name: "Video",
|
||||
tag: "video",
|
||||
sub_category: [
|
||||
{
|
||||
name: "Movies",
|
||||
tag: "movie",
|
||||
sub_category: [
|
||||
{
|
||||
name: "Movies DVDR",
|
||||
tag: "dvdr",
|
||||
},
|
||||
{
|
||||
name: "HD Movies",
|
||||
tag: "hd",
|
||||
},
|
||||
{
|
||||
name: "4k Movies",
|
||||
tag: "4k",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "TV",
|
||||
tag: "tv",
|
||||
sub_category: [
|
||||
{
|
||||
name: "HD TV",
|
||||
tag: "hd",
|
||||
},
|
||||
{
|
||||
name: "4k TV",
|
||||
tag: "4k",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Audio",
|
||||
tag: "audio",
|
||||
sub_category: [
|
||||
{
|
||||
name: "Music",
|
||||
tag: "music",
|
||||
},
|
||||
{
|
||||
name: "Audio Books",
|
||||
tag: "audio-book",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Applications",
|
||||
tag: "application",
|
||||
sub_category: [],
|
||||
},
|
||||
{
|
||||
name: "Other",
|
||||
tag: "other",
|
||||
sub_category: [
|
||||
{
|
||||
name: "Archives",
|
||||
tag: "archive",
|
||||
},
|
||||
],
|
||||
},
|
||||
] as Array<Category>;
|
||||
|
||||
export const TorrentKind = 2003 as EventKind;
|
||||
|
||||
export function FormatBytes(b: number, f?: number) {
|
||||
f ??= 2;
|
||||
if (b >= YiB) return (b / YiB).toFixed(f) + " YiB";
|
||||
if (b >= ZiB) return (b / ZiB).toFixed(f) + " ZiB";
|
||||
if (b >= EiB) return (b / EiB).toFixed(f) + " EiB";
|
||||
if (b >= PiB) return (b / PiB).toFixed(f) + " PiB";
|
||||
if (b >= TiB) return (b / TiB).toFixed(f) + " TiB";
|
||||
if (b >= GiB) return (b / GiB).toFixed(f) + " GiB";
|
||||
if (b >= MiB) return (b / MiB).toFixed(f) + " MiB";
|
||||
if (b >= kiB) return (b / kiB).toFixed(f) + " KiB";
|
||||
return b.toFixed(f) + " B";
|
||||
}
|
||||
|
||||
export const Trackers = [
|
||||
"udp://tracker.coppersurfer.tk:6969/announce",
|
||||
"udp://tracker.openbittorrent.com:6969/announce",
|
||||
"udp://open.stealth.si:80/announce",
|
||||
"udp://tracker.torrent.eu.org:451/announce",
|
||||
"udp://tracker.opentrackr.org:1337",
|
||||
];
|
36
src/element/button.tsx
Normal file
36
src/element/button.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import classNames from "classnames";
|
||||
import { HTMLProps, forwardRef, useState } from "react";
|
||||
|
||||
type ButtonProps = Omit<HTMLProps<HTMLButtonElement>, "onClick"> & {
|
||||
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => Promise<void> | void;
|
||||
};
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
|
||||
const [spinning, setSpinning] = useState(false);
|
||||
|
||||
async function clicking(e: React.MouseEvent<HTMLButtonElement>) {
|
||||
if (!props.onClick) return;
|
||||
e.preventDefault();
|
||||
try {
|
||||
setSpinning(true);
|
||||
await props?.onClick?.(e);
|
||||
} finally {
|
||||
setSpinning(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
type="button"
|
||||
className={classNames(
|
||||
"p-2 rounded flex gap-1 items-center justify-center bg-slate-800 hover:bg-slate-600",
|
||||
props.className,
|
||||
)}
|
||||
ref={ref}
|
||||
onClick={clicking}
|
||||
>
|
||||
{spinning ? "Loading.." : props.children}
|
||||
</button>
|
||||
);
|
||||
});
|
47
src/element/magnet.tsx
Normal file
47
src/element/magnet.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import { TaggedNostrEvent } from "@snort/system";
|
||||
import { Trackers } from "../const";
|
||||
import { Link, LinkProps } from "react-router-dom";
|
||||
|
||||
type MagnetLinkProps = Omit<LinkProps, "to"> & {
|
||||
item: TaggedNostrEvent;
|
||||
size?: number;
|
||||
};
|
||||
|
||||
export function MagnetLink({ item, size, ...props }: MagnetLinkProps) {
|
||||
const btih = item.tags.find((a) => a[0] === "btih")?.at(1);
|
||||
const name = item.tags.find((a) => a[0] === "title")?.at(1);
|
||||
const magnet = {
|
||||
xt: `urn:btih:${btih}`,
|
||||
dn: name,
|
||||
tr: Trackers,
|
||||
};
|
||||
const params = Object.entries(magnet)
|
||||
.map(([k, v]) => {
|
||||
if (Array.isArray(v)) {
|
||||
return v.map((a) => `${k}=${encodeURIComponent(a)}`).join("&");
|
||||
} else {
|
||||
return `${k}=${v as string}`;
|
||||
}
|
||||
})
|
||||
.flat()
|
||||
.join("&");
|
||||
const link = `magnet:?${params}`;
|
||||
|
||||
return (
|
||||
<Link {...props} to={link}>
|
||||
<svg width={size ?? 20} height={size ?? 20} version="1.1" viewBox="0 0 64 64" fill="currentColor">
|
||||
<path
|
||||
d="M54.5,9.5c-4.9-5-11.4-7.8-18.3-7.8c-6.5,0-12.6,2.4-17.2,7L3.6,24.3c-2.5,2.5-2.5,6.6,0,9.1l5.9,5.9c2.5,2.5,6.6,2.5,9.1,0
|
||||
l14.5-14.4c1.8-1.8,4.6-2.1,6.3-0.7c0.9,0.7,1.4,1.8,1.5,3c0.1,1.4-0.5,2.7-1.5,3.7L24.9,45.4c-2.5,2.5-2.5,6.6,0,9.1l5.9,5.9
|
||||
c1.2,1.2,2.9,1.9,4.5,1.9c1.6,0,3.3-0.6,4.5-1.9l15.5-15.5C64.8,35.4,64.5,19.6,54.5,9.5z M15.4,36c-0.7,0.7-2,0.7-2.7,0l-5.9-5.9
|
||||
c-0.7-0.7-0.7-2,0-2.7l5.1-5.1l8.6,8.6L15.4,36z M36.6,57.2c-0.7,0.7-2,0.7-2.7,0L28,51.3c-0.7-0.7-0.7-2,0-2.7l5.1-5.1l8.6,8.6
|
||||
L36.6,57.2z M52.2,41.7L45,48.9l-8.6-8.6l6.3-6.3c1.9-1.9,2.9-4.5,2.8-7.1c-0.1-2.5-1.3-4.7-3.2-6.3c-1.6-1.3-3.5-1.9-5.5-1.9
|
||||
c-2.5,0-5,1-6.9,2.9l-6.1,6.1l-8.6-8.6l7.2-7.2c3.7-3.6,8.6-5.7,13.9-5.7c0,0,0.1,0,0.1,0c5.7,0,11,2.3,15.1,6.5
|
||||
C59.5,21,59.9,34,52.2,41.7z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{props.children}
|
||||
</Link>
|
||||
);
|
||||
}
|
29
src/element/profile-image.tsx
Normal file
29
src/element/profile-image.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { NostrLink, NostrPrefix } from "@snort/system";
|
||||
import { useUserProfile } from "@snort/system-react";
|
||||
import { CSSProperties, HTMLProps } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
type ProfileImageProps = HTMLProps<HTMLDivElement> & {
|
||||
pubkey?: string;
|
||||
size?: number;
|
||||
};
|
||||
|
||||
export function ProfileImage({ pubkey, size, ...props }: ProfileImageProps) {
|
||||
const profile = useUserProfile(pubkey);
|
||||
const v = {
|
||||
backgroundImage: `url(${profile?.picture})`,
|
||||
} as CSSProperties;
|
||||
if (size) {
|
||||
v.width = `${size}px`;
|
||||
v.height = `${size}px`;
|
||||
}
|
||||
return (
|
||||
<Link to={pubkey ? `/p/${new NostrLink(NostrPrefix.Profile, pubkey).encode()}` : ""}>
|
||||
<div
|
||||
{...props}
|
||||
className="rounded-full aspect-square w-12 bg-slate-800 border border-slate-200 bg-cover bg-center"
|
||||
style={v}
|
||||
></div>
|
||||
</Link>
|
||||
);
|
||||
}
|
17
src/element/search.tsx
Normal file
17
src/element/search.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { Categories } from "../const";
|
||||
|
||||
export function Search() {
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex gap-3 flex-wrap">
|
||||
{Categories.map((a) => (
|
||||
<div className="flex gap-1" key={a.tag}>
|
||||
<input type="checkbox" />
|
||||
<label>{a.name}</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<input type="text" placeholder="Search.." className="p-3 rounded grow" />
|
||||
</div>
|
||||
);
|
||||
}
|
11
src/element/torrent-list.css
Normal file
11
src/element/torrent-list.css
Normal file
@ -0,0 +1,11 @@
|
||||
.torrent-list {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.torrent-list td,
|
||||
.torrent-list th {
|
||||
border: 1px solid #333;
|
||||
padding: 0px 5px;
|
||||
font-size: 14px;
|
||||
}
|
59
src/element/torrent-list.tsx
Normal file
59
src/element/torrent-list.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import "./torrent-list.css";
|
||||
import { hexToBech32 } from "@snort/shared";
|
||||
import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||
import { useUserProfile } from "@snort/system-react";
|
||||
import { FormatBytes } from "../const";
|
||||
import { Link } from "react-router-dom";
|
||||
import { MagnetLink } from "./magnet";
|
||||
|
||||
export function TorrentList({ items }: { items: Array<TaggedNostrEvent> }) {
|
||||
return (
|
||||
<table className="torrent-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Category</th>
|
||||
<th>Name</th>
|
||||
<th>Uploaded</th>
|
||||
<th></th>
|
||||
<th>Size</th>
|
||||
<th>From</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((a) => (
|
||||
<TorrentTableEntry item={a} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
function TorrentTableEntry({ item }: { item: TaggedNostrEvent }) {
|
||||
const profile = useUserProfile(item.pubkey);
|
||||
const name = item.tags.find((a) => a[0] === "title")?.at(1);
|
||||
const size = Number(item.tags.find((a) => a[0] === "size")?.at(1));
|
||||
const npub = hexToBech32("npub", item.pubkey);
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
{item.tags
|
||||
.filter((a) => a[0] === "t")
|
||||
.map((a) => a[1])
|
||||
.join(" > ")}
|
||||
</td>
|
||||
<td>
|
||||
<Link to={`/e/${NostrLink.fromEvent(item).encode()}`} state={item}>
|
||||
{name}
|
||||
</Link>
|
||||
</td>
|
||||
<td>{new Date(item.created_at * 1000).toLocaleDateString()}</td>
|
||||
<td>
|
||||
<MagnetLink item={item} />
|
||||
</td>
|
||||
<td>{FormatBytes(size)}</td>
|
||||
<td>
|
||||
<Link to={`/p/${npub}`}>{profile?.name ?? npub.slice(0, 12)}</Link>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
18
src/element/trending.tsx
Normal file
18
src/element/trending.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { NoteCollection, RequestBuilder } from "@snort/system";
|
||||
import { TorrentKind } from "../const";
|
||||
import { useRequestBuilder } from "@snort/system-react";
|
||||
import { TorrentList } from "./torrent-list";
|
||||
|
||||
export function LatestTorrents() {
|
||||
const sub = new RequestBuilder("torrents:latest");
|
||||
sub.withFilter().kinds([TorrentKind]).limit(100);
|
||||
|
||||
const latest = useRequestBuilder(NoteCollection, sub);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3>Latest Torrents</h3>
|
||||
<TorrentList items={latest.data ?? []} />
|
||||
</>
|
||||
);
|
||||
}
|
34
src/index.css
Normal file
34
src/index.css
Normal file
@ -0,0 +1,34 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html,
|
||||
body {
|
||||
background-color: black;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 32px;
|
||||
}
|
||||
h2 {
|
||||
font-size: 28px;
|
||||
}
|
||||
h3 {
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="number"],
|
||||
textarea {
|
||||
color: black;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
a:not([href="/"], :has(button)) {
|
||||
text-decoration: dotted;
|
||||
text-decoration-line: underline;
|
||||
}
|
42
src/login.tsx
Normal file
42
src/login.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import { ExternalStore } from "@snort/shared";
|
||||
import { useSyncExternalStore } from "react";
|
||||
|
||||
export interface LoginSession {
|
||||
publicKey: string;
|
||||
}
|
||||
class LoginStore extends ExternalStore<LoginSession | undefined> {
|
||||
#session?: LoginSession;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const s = window.localStorage.getItem("session");
|
||||
if (s) {
|
||||
this.#session = JSON.parse(s);
|
||||
}
|
||||
}
|
||||
|
||||
takeSnapshot() {
|
||||
return this.#session ? { ...this.#session } : undefined;
|
||||
}
|
||||
|
||||
login(pubkey: string) {
|
||||
this.#session = {
|
||||
publicKey: pubkey,
|
||||
};
|
||||
this.#save();
|
||||
}
|
||||
|
||||
#save() {
|
||||
window.localStorage.setItem("session", JSON.stringify(this.#session));
|
||||
this.notifyChange();
|
||||
}
|
||||
}
|
||||
|
||||
export const LoginState = new LoginStore();
|
||||
|
||||
export function useLogin() {
|
||||
return useSyncExternalStore(
|
||||
(c) => LoginState.hook(c),
|
||||
() => LoginState.snapshot(),
|
||||
);
|
||||
}
|
52
src/main.tsx
Normal file
52
src/main.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import "./index.css";
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { RouteObject, RouterProvider, createBrowserRouter } from "react-router-dom";
|
||||
import { Layout } from "./page/layout";
|
||||
import { HomePage } from "./page/home";
|
||||
import { NostrSystem } from "@snort/system";
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
import { ProfilePage } from "./page/profile";
|
||||
import { NewPage } from "./page/new";
|
||||
import { TorrentPage } from "./page/torrent";
|
||||
|
||||
const System = new NostrSystem({});
|
||||
const Routes = [
|
||||
{
|
||||
element: <Layout />,
|
||||
loader: async () => {
|
||||
await System.Init();
|
||||
for (const r of ["wss://nos.lol", "wss://relay.damus.io"]) {
|
||||
await System.ConnectToRelay(r, { read: true, write: true });
|
||||
}
|
||||
return null;
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "/",
|
||||
element: <HomePage />,
|
||||
},
|
||||
{
|
||||
path: "/p/:id",
|
||||
element: <ProfilePage />,
|
||||
},
|
||||
{
|
||||
path: "/new",
|
||||
element: <NewPage />,
|
||||
},
|
||||
{
|
||||
path: "/e/:id",
|
||||
element: <TorrentPage />,
|
||||
},
|
||||
],
|
||||
},
|
||||
] as Array<RouteObject>;
|
||||
|
||||
const Router = createBrowserRouter(Routes);
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<SnortContext.Provider value={System}>
|
||||
<RouterProvider router={Router} />
|
||||
</SnortContext.Provider>
|
||||
</React.StrictMode>,
|
||||
);
|
11
src/page/home.tsx
Normal file
11
src/page/home.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { Search } from "../element/search";
|
||||
import { LatestTorrents } from "../element/trending";
|
||||
|
||||
export function HomePage() {
|
||||
return (
|
||||
<>
|
||||
<Search />
|
||||
<LatestTorrents />
|
||||
</>
|
||||
);
|
||||
}
|
42
src/page/layout.tsx
Normal file
42
src/page/layout.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import { Link, Outlet } from "react-router-dom";
|
||||
import { Button } from "../element/button";
|
||||
import { LoginSession, LoginState, useLogin } from "../login";
|
||||
import { ProfileImage } from "../element/profile-image";
|
||||
|
||||
export function Layout() {
|
||||
const login = useLogin();
|
||||
|
||||
async function DoLogin() {
|
||||
if ("nostr" in window) {
|
||||
const pubkey = await window.nostr?.getPublicKey();
|
||||
if (pubkey) {
|
||||
LoginState.login(pubkey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto">
|
||||
<header className="flex justify-between items-center p-1">
|
||||
<Link to={"/"}>
|
||||
<h1 className="font-bold uppercase">dtan.xyz</h1>
|
||||
</Link>
|
||||
{login ? <LoggedInHeader login={login} /> : <Button onClick={DoLogin}>Login</Button>}
|
||||
</header>
|
||||
<div className="p-1">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LoggedInHeader({ login }: { login: LoginSession }) {
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<ProfileImage pubkey={login.publicKey} />
|
||||
<Link to="/new">
|
||||
<Button>+ Create</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
245
src/page/new.tsx
Normal file
245
src/page/new.tsx
Normal file
@ -0,0 +1,245 @@
|
||||
import { ReactNode, useContext, useState } from "react";
|
||||
import { Categories, Category, TorrentKind } from "../const";
|
||||
import { Button } from "../element/button";
|
||||
import { EventPublisher, Nip7Signer } from "@snort/system";
|
||||
import { useLogin } from "../login";
|
||||
import { dedupe } from "@snort/shared";
|
||||
import * as bencode from "../bencode";
|
||||
import { sha1 } from "@noble/hashes/sha1";
|
||||
import { bytesToHex } from "@noble/hashes/utils";
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
|
||||
async function openFile(): Promise<File | undefined> {
|
||||
return new Promise((resolve) => {
|
||||
const elm = document.createElement("input");
|
||||
let lock = false;
|
||||
elm.type = "file";
|
||||
elm.accept = ".torrent";
|
||||
const handleInput = (e: Event) => {
|
||||
lock = true;
|
||||
const elm = e.target as HTMLInputElement;
|
||||
if ((elm.files?.length ?? 0) > 0) {
|
||||
resolve(elm.files![0]);
|
||||
} else {
|
||||
resolve(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
elm.onchange = (e) => handleInput(e);
|
||||
elm.click();
|
||||
window.addEventListener(
|
||||
"focus",
|
||||
() => {
|
||||
setTimeout(() => {
|
||||
if (!lock) {
|
||||
console.debug("FOCUS WINDOW UPLOAD");
|
||||
resolve(undefined);
|
||||
}
|
||||
}, 300);
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function NewPage() {
|
||||
const login = useLogin();
|
||||
const system = useContext(SnortContext);
|
||||
|
||||
const [obj, setObj] = useState({
|
||||
name: "",
|
||||
desc: "",
|
||||
btih: "",
|
||||
tags: [] as Array<string>,
|
||||
files: [] as Array<{
|
||||
name: string;
|
||||
size: number;
|
||||
}>,
|
||||
});
|
||||
|
||||
async function loadTorrent() {
|
||||
const f = await openFile();
|
||||
if (f) {
|
||||
const buf = await f.arrayBuffer();
|
||||
const torrent = bencode.decode(new Uint8Array(buf)) as Record<string, bencode.BencodeValue>;
|
||||
const infoBuf = bencode.encode(torrent["info"]);
|
||||
console.debug(torrent);
|
||||
const dec = new TextDecoder();
|
||||
const info = torrent["info"] as {
|
||||
files?: Array<{ length: number; path: Array<Uint8Array> }>;
|
||||
length: number;
|
||||
name: Uint8Array;
|
||||
};
|
||||
|
||||
setObj({
|
||||
name: dec.decode(info.name),
|
||||
desc: dec.decode(torrent["comment"] as Uint8Array | undefined) ?? "",
|
||||
btih: bytesToHex(sha1(infoBuf)),
|
||||
tags: [],
|
||||
files: (info.files ?? [{ length: info.length, path: [info.name] }]).map((a) => ({
|
||||
size: a.length,
|
||||
name: dec.decode(a.path[0]),
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function publish() {
|
||||
if (!login) return;
|
||||
const signer = new Nip7Signer();
|
||||
const builder = new EventPublisher(signer, login.publicKey);
|
||||
|
||||
const ev = await builder.generic((eb) => {
|
||||
const v = eb
|
||||
.kind(TorrentKind)
|
||||
.content(obj.desc)
|
||||
.tag(["title", obj.name])
|
||||
.tag(["size", String(obj.files.reduce((acc, v) => (acc += v.size), 0))])
|
||||
.tag(["btih", obj.btih]);
|
||||
|
||||
obj.tags.forEach((t) => v.tag(["t", t]));
|
||||
obj.files.forEach((f) => v.tag(["file", f.name, String(f.size)]));
|
||||
|
||||
return v;
|
||||
});
|
||||
console.debug(ev);
|
||||
|
||||
if (ev) {
|
||||
await system.BroadcastEvent(ev);
|
||||
}
|
||||
}
|
||||
|
||||
function renderCategories(a: Category, tags: Array<string>): ReactNode {
|
||||
return (
|
||||
<>
|
||||
<div className="flex gap-1 bg-slate-500 p-1 rounded">
|
||||
<input
|
||||
type="radio"
|
||||
value={tags.join(",")}
|
||||
name="category"
|
||||
checked={obj.tags.join(",") === tags.join(",")}
|
||||
onChange={(e) =>
|
||||
setObj((o) => ({
|
||||
...o,
|
||||
tags: e.target.checked ? dedupe(e.target.value.split(",")) : [],
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<label>{a?.name}</label>
|
||||
</div>
|
||||
{a.sub_category?.map((b) => renderCategories(b, [...tags, b.tag]))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>New</h1>
|
||||
<div className="flex gap-1">
|
||||
<Button onClick={loadTorrent}>Import from Torrent</Button>
|
||||
<Button>Import from Magnet</Button>
|
||||
</div>
|
||||
<h2>Torrent Info</h2>
|
||||
<form className="flex flex-col gap-2">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1 flex flex-col gap-1">
|
||||
<label>Title</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="raw noods"
|
||||
value={obj.name}
|
||||
onChange={(e) => setObj((o) => ({ ...o, name: e.target.value }))}
|
||||
/>
|
||||
<label>Info Hash</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="hex"
|
||||
value={obj.btih}
|
||||
onChange={(e) => setObj((o) => ({ ...o, btih: e.target.value }))}
|
||||
/>
|
||||
<label>Category</label>
|
||||
<div className="flex flex-col gap-1">
|
||||
{Categories.map((a) => (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="font-bold bg-slate-800 p-1">{a.name}</div>
|
||||
<div className="flex gap-1 flex-wrap">{renderCategories(a, [a.tag])}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col gap-1">
|
||||
<label>Description</label>
|
||||
<textarea
|
||||
rows={20}
|
||||
className="font-mono text-xs"
|
||||
value={obj.desc}
|
||||
onChange={(e) => setObj((o) => ({ ...o, desc: e.target.value }))}
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Files</h2>
|
||||
<div className="flex flex-col gap-2">
|
||||
{obj.files.map((a, i) => (
|
||||
<div className="flex gap-1">
|
||||
<input
|
||||
type="text"
|
||||
value={a.name}
|
||||
className="flex-1"
|
||||
placeholder="collection1/IMG_00001.jpg"
|
||||
onChange={(e) =>
|
||||
setObj((o) => ({
|
||||
...o,
|
||||
files: o.files.map((f, ii) => {
|
||||
if (ii === i) {
|
||||
return { ...f, name: e.target.value };
|
||||
}
|
||||
return f;
|
||||
}),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
value={a.size}
|
||||
min={0}
|
||||
placeholder="69000"
|
||||
onChange={(e) =>
|
||||
setObj((o) => ({
|
||||
...o,
|
||||
files: o.files.map((f, ii) => {
|
||||
if (ii === i) {
|
||||
return { ...f, size: Number(e.target.value) };
|
||||
}
|
||||
return f;
|
||||
}),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
onClick={() =>
|
||||
setObj((o) => ({
|
||||
...o,
|
||||
files: o.files.filter((_, ii) => i !== ii),
|
||||
}))
|
||||
}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
onClick={() =>
|
||||
setObj((o) => ({
|
||||
...o,
|
||||
files: [...o.files, { name: "", size: 0 }],
|
||||
}))
|
||||
}
|
||||
>
|
||||
Add File
|
||||
</Button>
|
||||
<Button onClick={publish}>Publish</Button>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
37
src/page/profile.tsx
Normal file
37
src/page/profile.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { useUserProfile } from "@snort/system-react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { ProfileImage } from "../element/profile-image";
|
||||
import { parseNostrLink } from "@snort/system";
|
||||
import { LatestTorrents } from "../element/trending";
|
||||
|
||||
export function ProfilePage() {
|
||||
const params = useParams();
|
||||
const id = params.id as string;
|
||||
const link = parseNostrLink(id);
|
||||
|
||||
if (!link) return;
|
||||
return (
|
||||
<>
|
||||
<ProfileSection pubkey={link.id} />
|
||||
<LatestTorrents />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function ProfileSection({ pubkey }: { pubkey: string }) {
|
||||
const profile = useUserProfile(pubkey);
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<ProfileImage pubkey={pubkey} size={240} />
|
||||
<div className="flex flex-col gap-2">
|
||||
<h2>{profile?.name}</h2>
|
||||
<p>{profile?.about}</p>
|
||||
{profile?.website && (
|
||||
<Link to={profile.website} target="_blank">
|
||||
{new URL(profile.website).hostname}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
49
src/page/torrent.tsx
Normal file
49
src/page/torrent.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import { unwrap } from "@snort/shared";
|
||||
import { NoteCollection, RequestBuilder, TaggedNostrEvent, parseNostrLink } from "@snort/system";
|
||||
import { useRequestBuilder } from "@snort/system-react";
|
||||
import { useLocation, useParams } from "react-router-dom";
|
||||
import { FormatBytes, TorrentKind } from "../const";
|
||||
import { ProfileImage } from "../element/profile-image";
|
||||
import { MagnetLink } from "../element/magnet";
|
||||
|
||||
export function TorrentPage() {
|
||||
const location = useLocation();
|
||||
const { id } = useParams();
|
||||
const evState = "kind" in location.state ? (location.state as TaggedNostrEvent) : undefined;
|
||||
|
||||
const rb = new RequestBuilder("torrent:event");
|
||||
rb.withFilter()
|
||||
.kinds([TorrentKind])
|
||||
.link(parseNostrLink(unwrap(id)));
|
||||
|
||||
const evNew = useRequestBuilder(NoteCollection, evState ? null : rb);
|
||||
|
||||
const ev = evState ?? evNew.data?.at(0);
|
||||
if (!ev) return;
|
||||
return <TorrentDetail item={ev} />;
|
||||
}
|
||||
|
||||
export function TorrentDetail({ item }: { item: TaggedNostrEvent }) {
|
||||
const name = item.tags.find((a) => a[0] === "title")?.at(1);
|
||||
const size = Number(item.tags.find((a) => a[0] === "size")?.at(1));
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex gap-2 items-center text-xl">
|
||||
<ProfileImage pubkey={item.pubkey} />
|
||||
{name}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 bg-slate-700 p-2 rounded">
|
||||
<div>Size: {FormatBytes(size)}</div>
|
||||
<div>Uploaded: {new Date(item.created_at * 1000).toLocaleDateString()}</div>
|
||||
<div>
|
||||
<MagnetLink item={item} className="flex gap-1 items-center">
|
||||
Get this torrent
|
||||
</MagnetLink>
|
||||
</div>
|
||||
</div>
|
||||
<h3>Description</h3>
|
||||
<pre className="font-mono text-xs bg-slate-700 p-2 rounded overflow-y-auto">{item.content}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
8
tailwind.config.js
Normal file
8
tailwind.config.js
Normal file
@ -0,0 +1,8 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
24
tsconfig.json
Normal file
24
tsconfig.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
10
tsconfig.node.json
Normal file
10
tsconfig.node.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
7
vite.config.ts
Normal file
7
vite.config.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
});
|
Loading…
Reference in New Issue
Block a user