mirror of
https://github.com/cupcakearmy/cryptgeon-raycast.git
synced 2025-01-22 07:16:28 +00:00
initial push
This commit is contained in:
commit
0bb793f5a0
4
.eslintrc.json
Normal file
4
.eslintrc.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"extends": ["@raycast"]
|
||||||
|
}
|
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal 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
4
.prettierrc
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 120,
|
||||||
|
"singleQuote": false
|
||||||
|
}
|
3
CHANGELOG.md
Normal file
3
CHANGELOG.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Cryptgeon Changelog
|
||||||
|
|
||||||
|
## [Initial Version] - 2024-08-22
|
11
README.md
Normal file
11
README.md
Normal 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
BIN
assets/extension-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 56 KiB |
BIN
metadata/cryptgeon-1.png
Normal file
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
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
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
2197
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
70
package.json
Normal file
70
package.json
Normal 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
104
src/create-note.tsx
Normal 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
13
src/fetch.ts
Normal 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;
|
||||||
|
}
|
8
src/send-encrypted-text.ts
Normal file
8
src/send-encrypted-text.ts
Normal 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
43
src/shared.ts
Normal 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
16
tsconfig.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user