action to toggle open with x or y

This commit is contained in:
Lone 2025-03-13 17:23:27 +00:00
parent b6da438a7d
commit 3aea45b6c5
3 changed files with 184 additions and 204 deletions

10
dist/index-nip.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1,190 +0,0 @@
import { render, Fragment } from "preact"
import { useEffect, useRef, useState } from "preact/hooks"
import { ui, open } from "@kksh/api/ui/custom"
import { Button } from "@kksh/react"
import { Icon } from "@iconify/react";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
ThemeProvider
} from "@kksh/react"
import {
OpenInNewWindowIcon,
GitHubLogoIcon,
} from "@radix-ui/react-icons"
import "./index.css"
// Function to fetch NIPs from GitHub
async function fetchNostrNips() {
try {
// Fetch the README.md file from the repository
const response = await fetch('https://api.github.com/repos/nostr-protocol/nips/contents/README.md');
const fileData = await response.json();
// Decode content from base64
const content = atob(fileData.content);
// Regular expression to match NIP entries in the list
// Format is like: - [NIP-01: Basic protocol flow description](01.md)
const nipRegex = /\- \[NIP-(\d+)\: (.*?)\]\((\d+\.md)\)/g;
const nips = [];
let match;
// Find all matches in the content
while ((match = nipRegex.exec(content)) !== null) {
const nipNumber = match[1].padStart(2, '0'); // Pad to ensure consistent format like "01"
const nipNumberNoPad = parseInt(nipNumber); // remove leading zeros
const title = <Fragment>{match[2].split(/`([^`]+)`/).map((part, i) =>
i % 2 === 0 ? part : <code className="bg-gray-100 dark:bg-gray-800 rounded px-1.5 py-0.5" key={i}>{part}</code>
)}</Fragment>;
const filename = match[3];
nips.push({
nip: nipNumber,
title: title,
rawTitle: match[2],
urlGithub: `https://github.com/nostr-protocol/nips/blob/master/${filename}`,
urlNostrCom: `https://nips.nostr.com/${nipNumberNoPad}`,
// We're not fetching full content anymore, but we need to provide a placeholder
// for the filtering function that uses content
content: `NIP-${nipNumber}: ${title}`
});
}
// Sort NIPs by number
return nips.sort((a, b) => parseInt(a.nip) - parseInt(b.nip));
} catch (error) {
console.error('Error fetching NIPs:', error);
return [];
}
}
const App = () => {
const [input, setInput] = useState("")
const seachInputEle = useRef(null)
const [nips, setNips] = useState([])
const [loading, setLoading] = useState(false)
useEffect(() => {
ui.registerDragRegion()
ui.showMoveButton({
bottom: 0.2,
left: 0.2
})
// Fetch NIPs when component mounts
const loadNips = async () => {
setLoading(true)
try {
const fetchedNips = await fetchNostrNips()
setNips(fetchedNips)
} catch (error) {
console.error('Failed to load NIPs:', error)
} finally {
setLoading(false)
}
}
loadNips()
}, [])
function onKeyDown(e) {
if (e.key === "Escape") {
if (input.length === 0) {
ui.goBack()
} else {
setInput("")
}
}
}
// Filter NIPs based on input
const filteredNips = input
? nips.filter(nip =>
nip.rawTitle.toLowerCase().includes(input.toLowerCase()) ||
nip.nip.includes(input) ||
nip.content.toLowerCase().includes(input.toLowerCase())
)
: nips
return (
<ThemeProvider>
<main className="h-screen">
<Command
onValueChange={(v) => {
setValue(v)
}}
>
<CommandInput
autoFocus
ref={seachInputEle}
placeholder="Search NIPs by number or title..."
style={{height: '3.25rem'}}
onInput={(e) => {
setInput(e.target.value)
}}
value={input}
onKeyDown={onKeyDown}
>
<div className="h-8 w-8"></div>
</CommandInput>
<CommandList className="h-full">
{loading ? (
<div className="p-4 text-center">Loading NIPs...</div>
) : (
<Fragment>
<CommandEmpty>No NIPs found.</CommandEmpty>
<CommandGroup heading="Nostr Implementation Possibilities">
{filteredNips.map((nip) => (
<CommandItem
key={nip.nip}
>
<div className="flex items-center justify-between w-full">
<div className="truncate">
<span className="font-bold">NIP-{nip.nip}</span>: {nip.title}
</div>
<div className="flex space-x-2 ml-2">
<Button
style={{padding: '.1em .4em'}}
onClick={(e) => {
e.stopPropagation();
open.url(nip.urlNostrCom);
}}
title="Open on nips.nostr.com"
>
<Icon icon="game-icons:ostrich" width="20" height="20" />
</Button>
<Button
style={{padding: '.1em .4em'}}
onClick={(e) => {
e.stopPropagation();
open.url(nip.urlGithub);
}}
title="Open on GitHub"
>
<Icon icon="mdi:github" width="20" height="20" />
</Button>
</div>
</div>
</CommandItem>
))}
</CommandGroup>
</Fragment>
)}
</CommandList>
</Command>
</main>
</ThemeProvider>
)
}
render(<App />, document.getElementById("root"))

View File

@ -1,6 +1,5 @@
import {
Action,
clipboard,
expose,
Icon,
IconEnum,
@ -9,6 +8,7 @@ import {
TemplateUiCommand,
toast,
ui,
db
} from "@kksh/api/ui/template";
interface Nip {
@ -22,6 +22,7 @@ interface Nip {
class NostrOpenSpecificNip extends TemplateUiCommand {
private nips: Nip[] = [];
private preferences: string = "";
private loading: boolean = false;
private searchQuery: string = "";
@ -29,6 +30,7 @@ class NostrOpenSpecificNip extends TemplateUiCommand {
this.loading = true;
this.updateUI();
try {
// Fetch NIPs
this.nips = await this.fetchNostrNips();
} catch (error) {
console.error('Failed to load NIPs:', error);
@ -36,31 +38,199 @@ class NostrOpenSpecificNip extends TemplateUiCommand {
this.loading = false;
this.updateUI();
}
try {
const data = await db.retrieveAll({
fields: ["data", "search_text"],
});
if (data.length > 0 && data[0].data) {
this.preferences = JSON.parse(data[0].data) ? JSON.parse(data[0].data) : 'nostr';
this.updateUI();
} else {
this.preferences = 'nostr';
this.updateUI();
}
} catch (error) {
console.error('Failed to load preferences:', error);
}
}
// Function to fetch NIPs from GitHub
async fetchNostrNips(): Promise<Nip[]> {
return [];
try {
// Fetch the README.md file from the repository
const response = await fetch('https://api.github.com/repos/nostr-protocol/nips/contents/README.md');
const fileData = await response.json();
// Decode content from base64
const content = atob(fileData.content);
// Regular expression to match NIP entries in the list
// Format is like: - [NIP-01: Basic protocol flow description](01.md)
const nipRegex = /\- \[NIP-(\d+)\: (.*?)\]\((\d+\.md)\)/g;
const nips: Nip[] = [];
let match;
// Find all matches in the content
while ((match = nipRegex.exec(content)) !== null) {
const nipNumber = match[1].padStart(2, '0'); // Pad to ensure consistent format like "01"
const nipNumberNoPad = parseInt(nipNumber); // remove leading zeros
const title = match[2];
const filename = match[3];
nips.push({
nip: nipNumber,
title: title,
rawTitle: title, // Store raw title without formatting
urlGithub: `https://github.com/nostr-protocol/nips/blob/master/${filename}`,
urlNostrCom: `https://nips.nostr.com/${nipNumberNoPad}`,
// We include content for filtering
content: `NIP-${nipNumber}: ${title}`
});
}
// Sort NIPs by number
return nips.sort((a, b) => parseInt(a.nip) - parseInt(b.nip));
} catch (error) {
console.error('Error fetching NIPs:', error);
return [];
}
}
// Filter NIPs based on search query
getFilteredNips(): Nip[] {
return [];
if (!this.searchQuery) {
return this.nips;
}
const query = this.searchQuery.toLowerCase();
return this.nips.filter(nip =>
nip.rawTitle.toLowerCase().includes(query) ||
nip.nip.includes(query) ||
nip.content.toLowerCase().includes(query)
);
}
// Handle search input change
handleSearchInput(query: string): void {
async onSearchTermChange(query: string): Promise<void> {
this.searchQuery = query;
this.updateUI();
}
// Open URL in browser
handleOpenUrl(url: string): void {
open.url(url);
// Create action panel for the footer
getFooterActions(): Action[] {
let actions = [
new Action.Action({
title: "Always open on nips.nostr.com",
value: "open-nostr",
icon: new Icon({
type: IconEnum.Iconify,
value: "game-icons:ostrich",
}),
}),
new Action.Action({
title: "Always open on GitHub",
value: "open-github",
icon: new Icon({
type: IconEnum.Iconify,
value: "mdi:github",
}),
}),
];
if (this.preferences === "github") {
actions.reverse()
}
return actions;
}
updateUI() {
return 'Nostr NIPs';
async updateUI() {
console.log('updateUI', this.preferences);
return ui
.setSearchBarPlaceholder("Search NIPs by number or title… or k[X] 🥳")
.then(() => {
const filteredNips = this.getFilteredNips();
if (this.loading) {
return ui.render(new List.List({
sections: [
new List.Section({
title: "Loading...",
items: []
})
]
}));
}
if (filteredNips.length === 0) {
return ui.render(new List.List({
sections: [
new List.Section({
title: "No NIPs found",
items: []
})
]
}));
}
return ui.render(
new List.List({
sections: [
new List.Section({
title: "Nostr Implementation Possibilities",
items: filteredNips.map(
(nip) =>
new List.Item({
title: `NIP-${nip.nip}: ${nip.title}`,
value: this.preferences === "nostr" ? nip.urlNostrCom : nip.urlGithub,
icon: new Icon({
type: IconEnum.Iconify,
value: "majesticons:open",
}),
})
),
}),
],
actions: new Action.ActionPanel({
items: this.getFooterActions(),
}),
})
);
});
}
onListItemSelected(value: string): Promise<void> {
return open.url(value);
}
async onActionSelected(value: string): Promise<void> {
if (value === "open-nostr") {
await db.deleteAll();
await db.add({
data: JSON.stringify('nostr'),
dataType: "preference",
searchText: "open_with",
});
this.preferences = 'nostr';
this.updateUI();
}
if (value === "open-github") {
await db.deleteAll();
await db.add({
data: JSON.stringify('github'),
dataType: "preference",
searchText: "open_with",
});
this.preferences = 'github';
this.updateUI();
}
return Promise.resolve();
}
}