initial push

This commit is contained in:
Niccolo Borgioli 2024-08-23 11:37:53 +02:00
commit 0bb793f5a0
16 changed files with 2486 additions and 0 deletions

4
.eslintrc.json Normal file
View File

@ -0,0 +1,4 @@
{
"root": true,
"extends": ["@raycast"]
}

13
.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
# Raycast specific files
raycast-env.d.ts
.raycast-swift-build
.swiftpm
compiled_raycast_swift
# misc
.DS_Store

4
.prettierrc Normal file
View File

@ -0,0 +1,4 @@
{
"printWidth": 120,
"singleQuote": false
}

3
CHANGELOG.md Normal file
View File

@ -0,0 +1,3 @@
# Cryptgeon Changelog
## [Initial Version] - 2024-08-22

11
README.md Normal file
View File

@ -0,0 +1,11 @@
# Cryptgeon
Official Raycast extension for [Cryptgeon](https://github.com/cupcakearmy/cryptgeon).
## Features
- Easily create and share encrypted text or files.
- Notes are limited by views or time expiration.
- Client side encryption.
- No account, tracking, ads, or analytics.
- Open source.

BIN
assets/extension-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

BIN
metadata/cryptgeon-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
metadata/cryptgeon-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
metadata/cryptgeon-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

2197
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

70
package.json Normal file
View File

@ -0,0 +1,70 @@
{
"$schema": "https://www.raycast.com/schemas/extension.json",
"name": "cryptgeon",
"description": "Create and share encrypted notes",
"categories": [
"Security",
"Productivity"
],
"license": "MIT",
"author": "cupcakearmy",
"scripts": {
"build": "ray build -e dist",
"dev": "ray develop",
"fix-lint": "ray lint --fix",
"lint": "ray lint",
"prepublishOnly": "echo \"\\n\\nIt seems like you are trying to publish the Raycast extension to npm.\\n\\nIf you did intend to publish it to npm, remove the \\`prepublishOnly\\` script and rerun \\`npm publish\\` again.\\nIf you wanted to publish it to the Raycast Store instead, use \\`npm run publish\\` instead.\\n\\n\" && exit 1",
"publish": "npx @raycast/api@latest publish"
},
"dependencies": {
"@raycast/api": "^1.81.2",
"@raycast/utils": "^1.16.5",
"cryptgeon": "^2.7.0",
"node-fetch": "^3.3.2",
"pretty-bytes": "^6.1.1",
"uhrwerk": "^1.1.2"
},
"devDependencies": {
"@raycast/eslint-config": "^1.0.11",
"@types/node": "22.5.0",
"@types/react": "18.3.4",
"eslint": "^9.9.0",
"prettier": "^3.3.3",
"typescript": "^5.5.4"
},
"packageManager": "npm@10.8.2",
"icon": "extension-icon.png",
"commands": [
{
"name": "create-note",
"title": "Create note",
"description": "Create and share an encrypted note",
"mode": "view"
},
{
"name": "send-encrypted-text",
"title": "Send Encrypted Text",
"description": "Quickly create a once view text note",
"mode": "no-view",
"arguments": [
{
"name": "text",
"placeholder": "Text",
"type": "text",
"required": true
}
]
}
],
"preferences": [
{
"name": "server",
"title": "Server",
"description": "Which server to use",
"required": true,
"type": "textfield",
"default": "https://cryptgeon.org"
}
],
"title": "Cryptgeon"
}

104
src/create-note.tsx Normal file
View File

@ -0,0 +1,104 @@
import "./fetch.js";
import { Action, ActionPanel, Form, getPreferenceValues, Icon } from "@raycast/api";
import { useForm } from "@raycast/utils";
// @ts-ignore
import { status as getStatus, setOptions, Status } from "cryptgeon";
import prettyBytes from "pretty-bytes";
import { useEffect, useState } from "react";
import { Duration } from "uhrwerk";
import { createNote } from "./shared.js";
type CreateNotePayload = {
type: string;
content: string;
files: string[];
limit: string;
views: string;
expiration: Date | null;
};
function getRelativeMinutes(date: Date): number {
const now = Date.now();
const diff = date.valueOf() - now;
return Math.ceil(diff / 1000 / 60);
}
export default function Command() {
const preferences = getPreferenceValues<Preferences>();
const [status, setStatus] = useState<Status | null>(null);
const expirationHuman = status ? new Duration(status.max_expiration, "minutes").humanize() : "...";
const { handleSubmit, itemProps, values } = useForm<CreateNotePayload>({
async onSubmit(values) {
await createNote(values.content ?? values.files, {
views: values.views ? Number.parseInt(values.views) : undefined,
expiration: values.expiration ? getRelativeMinutes(values.expiration) : undefined,
});
},
validation: {
views: (value) => {
if (!value) return;
const parsed = Number.parseInt(value);
if (Number.isNaN(parsed)) return "Must be a number";
if (parsed < 1) return "Must be greater than 0";
},
expiration: (value) => {
if (!value) return;
const minutes = getRelativeMinutes(value);
if (minutes < 1) return "Must be in the future";
if (status && status.max_expiration < minutes) return `Must be less than ${expirationHuman}`;
},
},
});
useEffect(() => {
setOptions({ server: preferences.server });
getStatus().then(setStatus);
}, []);
return (
<Form
actions={
<ActionPanel>
<Action.SubmitForm title="Submit" onSubmit={handleSubmit} />
</ActionPanel>
}
>
<Form.Dropdown title="Type" defaultValue="text" {...itemProps.type}>
<Form.Dropdown.Item value="text" title="Text" icon={Icon.Text} />
<Form.Dropdown.Item value="file" title="File" icon={Icon.Document} />
</Form.Dropdown>
{values.type === "text" ? (
<Form.TextArea {...itemProps.content} title="Content" />
) : (
<Form.FilePicker {...itemProps.files} />
)}
<Form.Separator />
<Form.Dropdown {...itemProps.limit} title="Limit by" defaultValue="view">
<Form.Dropdown.Item value="views" title="Views" icon={Icon.Eye} />
<Form.Dropdown.Item value="time" title="Time" icon={Icon.Calendar} />
</Form.Dropdown>
{values.limit === "views" ? (
<Form.TextField {...itemProps.views} defaultValue="1" />
) : (
<Form.DatePicker {...itemProps.expiration} title="Expiration" />
)}
<Form.Separator />
<Form.Description title="Server" text={status ? `${status.version} ${preferences.server}` : "..."} />
<Form.Description
title="Limits"
text={
status
? `Views: ${status.max_views} Expiration: ${expirationHuman} Size: ${prettyBytes(status.max_size)}`
: "..."
}
/>
</Form>
);
}

13
src/fetch.ts Normal file
View File

@ -0,0 +1,13 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
// https://github.com/node-fetch/node-fetch?tab=readme-ov-file#providing-global-access
import fetch, { Headers, Request, Response } from "node-fetch";
if (!globalThis.fetch) {
globalThis.fetch = fetch;
globalThis.Headers = Headers;
globalThis.Request = Request;
globalThis.Response = Response;
}

View File

@ -0,0 +1,8 @@
import "./fetch.js";
import { LaunchProps } from "@raycast/api";
import { createNote } from "./shared.js";
export default async function Command(props: LaunchProps<{ arguments: { text: string } }>) {
await createNote(props.arguments.text, { views: 1, expiration: undefined });
}

43
src/shared.ts Normal file
View File

@ -0,0 +1,43 @@
import { Clipboard, getPreferenceValues, PopToRootType, showHUD, showToast, Toast } from "@raycast/api";
// @ts-ignore
import { setOptions, upload } from "cryptgeon";
export type Preferences = {
server: string;
};
export const text = {
noteSuccess: "Note created. Link copied to Clipboard.",
};
function checkAndSetServer() {
const preferences = getPreferenceValues<Preferences>();
try {
setOptions({
server: preferences.server,
});
} catch (e) {
if (e instanceof TypeError) {
showToast({
style: Toast.Style.Failure,
title: "Invalid server provided",
message: `"${preferences.server}"`,
});
}
}
}
export async function createNote(...args: Parameters<typeof upload>) {
checkAndSetServer();
const toast = await showToast({
style: Toast.Style.Animated,
title: "Creating...",
});
const url = await upload(args[0], args[1]);
Clipboard.copy(url);
toast.hide();
showHUD(text.noteSuccess, {
clearRootSearch: true,
popToRootType: PopToRootType.Immediate,
});
}

16
tsconfig.json Normal file
View File

@ -0,0 +1,16 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"include": ["src/**/*", "raycast-env.d.ts"],
"compilerOptions": {
"lib": ["ES2023"],
"module": "commonjs",
"target": "ES2022",
"strict": true,
"isolatedModules": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"jsx": "react-jsx",
"resolveJsonModule": true
}
}