mirror of
https://github.com/ByteAtATime/raycast-linux.git
synced 2025-08-31 03:07:23 +00:00
fix(extensions): load data from extension specific endpoint
Originally, we were directly using the data provided by the paginated endoint. This was incorrect, as the endpoint did not provide a list of metadata files. This commit fixes that, as well as updating the store response schema to match.
This commit is contained in:
parent
c8b4cb58e0
commit
828be15da3
6 changed files with 122 additions and 61 deletions
|
@ -1,7 +1,6 @@
|
|||
<script lang="ts">
|
||||
import type { Datum } from '$lib/store';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { ArrowLeft } from '@lucide/svelte';
|
||||
import { type Extension, ExtensionSchema } from '$lib/store';
|
||||
import Icon from './Icon.svelte';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import ExtensionListView from './extensions/ExtensionListView.svelte';
|
||||
|
@ -14,6 +13,7 @@
|
|||
import HeaderInput from './HeaderInput.svelte';
|
||||
import { viewManager } from '$lib/viewManager.svelte';
|
||||
import ExtensionInstallConfirm from './extensions/ExtensionInstallConfirm.svelte';
|
||||
import { fetch } from '@tauri-apps/plugin-http';
|
||||
|
||||
type Props = {
|
||||
onBack: () => void;
|
||||
|
@ -27,7 +27,9 @@
|
|||
|
||||
let { onBack, onInstall }: Props = $props();
|
||||
|
||||
let selectedExtension = $state<Datum | null>(null);
|
||||
let selectedExtension = $state<Extension | null>(null);
|
||||
let detailedExtension = $state<Extension | null>(null);
|
||||
let isDetailLoading = $state(false);
|
||||
let expandedImageUrl = $state<string | null>(null);
|
||||
let isInstalling = $state(false);
|
||||
let vlistInstance = $state<VListHandle | null>(null);
|
||||
|
@ -42,6 +44,32 @@
|
|||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (selectedExtension && selectedExtension.id !== detailedExtension?.id) {
|
||||
detailedExtension = null;
|
||||
isDetailLoading = true;
|
||||
const fetchDetails = async () => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://backend.raycast.com/api/v1/extensions/${selectedExtension!.author.handle}/${selectedExtension!.name}`
|
||||
);
|
||||
if (!res.ok) throw new Error(`Failed to fetch extension details: ${res.status}`);
|
||||
const json = await res.json();
|
||||
const parsed = ExtensionSchema.parse(json);
|
||||
detailedExtension = parsed;
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch or parse extension details, using list data.', e);
|
||||
detailedExtension = selectedExtension;
|
||||
} finally {
|
||||
isDetailLoading = false;
|
||||
}
|
||||
};
|
||||
fetchDetails();
|
||||
} else if (!selectedExtension) {
|
||||
detailedExtension = null;
|
||||
}
|
||||
});
|
||||
|
||||
const handleScroll = () => {
|
||||
if (!vlistInstance) return;
|
||||
if (
|
||||
|
@ -69,15 +97,16 @@
|
|||
}
|
||||
|
||||
async function handleInstall() {
|
||||
if (!selectedExtension || isInstalling) return;
|
||||
const extensionToInstall = detailedExtension || selectedExtension;
|
||||
if (!extensionToInstall || isInstalling) return;
|
||||
isInstalling = true;
|
||||
try {
|
||||
const result = await invoke<{
|
||||
status: 'success' | 'requiresConfirmation';
|
||||
violations?: Violation[];
|
||||
}>('install_extension', {
|
||||
downloadUrl: selectedExtension.download_url,
|
||||
slug: selectedExtension.name,
|
||||
downloadUrl: extensionToInstall.download_url,
|
||||
slug: extensionToInstall.name,
|
||||
force: false
|
||||
});
|
||||
|
||||
|
@ -96,12 +125,13 @@
|
|||
|
||||
async function handleForceInstall() {
|
||||
showConfirmationDialog = false;
|
||||
if (!selectedExtension) return;
|
||||
const extensionToInstall = detailedExtension || selectedExtension;
|
||||
if (!extensionToInstall) return;
|
||||
isInstalling = true;
|
||||
try {
|
||||
await invoke('install_extension', {
|
||||
downloadUrl: selectedExtension.download_url,
|
||||
slug: selectedExtension.name,
|
||||
downloadUrl: extensionToInstall.download_url,
|
||||
slug: extensionToInstall.name,
|
||||
force: true
|
||||
});
|
||||
onInstall();
|
||||
|
@ -133,12 +163,15 @@
|
|||
/>
|
||||
<CategoryFilter />
|
||||
{/if}
|
||||
<LoadingIndicator isLoading={extensionsStore.isLoading && !selectedExtension} />
|
||||
<LoadingIndicator
|
||||
isLoading={(extensionsStore.isLoading && !selectedExtension) || isDetailLoading}
|
||||
/>
|
||||
</header>
|
||||
|
||||
{#if selectedExtension}
|
||||
{@const extensionToShow = detailedExtension || selectedExtension}
|
||||
<ExtensionDetailView
|
||||
extension={selectedExtension}
|
||||
extension={extensionToShow}
|
||||
{isInstalling}
|
||||
onInstall={handleInstall}
|
||||
onOpenLightbox={(imageUrl) => (expandedImageUrl = imageUrl)}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import type { Datum, Command as ExtensionCommand } from '$lib/store';
|
||||
import type { Extension, Command as ExtensionCommand } from '$lib/store';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { ArrowUpRight } from '@lucide/svelte';
|
||||
import Icon from '../Icon.svelte';
|
||||
|
@ -15,10 +15,9 @@
|
|||
import KeyboardShortcut from '../KeyboardShortcut.svelte';
|
||||
import { uiStore } from '$lib/ui.svelte';
|
||||
import { viewManager } from '$lib/viewManager.svelte';
|
||||
import type { PluginInfo } from '@raycast-linux/protocol';
|
||||
|
||||
type Props = {
|
||||
extension: Datum;
|
||||
extension: Extension;
|
||||
isInstalling: boolean;
|
||||
onInstall: () => void;
|
||||
onOpenLightbox: (imageUrl: string) => void;
|
||||
|
@ -79,6 +78,19 @@
|
|||
: []
|
||||
);
|
||||
|
||||
const screenshots = $derived.by(() => {
|
||||
if (extension.metadata && extension.metadata.length > 0) {
|
||||
return extension.metadata;
|
||||
}
|
||||
if (extension.metadata_count > 0) {
|
||||
return Array.from(
|
||||
{ length: extension.metadata_count },
|
||||
(_, i) => `${extension.readme_assets_path}metadata/${extension.name}-${i + 1}.png`
|
||||
);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
function handleOpenCommand(command: ExtensionCommand) {
|
||||
const pluginInfo = installedCommandsInfo.find((p) => p.commandName === command.name);
|
||||
if (pluginInfo) {
|
||||
|
@ -89,7 +101,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="flex grow flex-col gap-6 overflow-y-auto p-6">
|
||||
<div class="flex grow flex-col gap-6 overflow-x-hidden overflow-y-auto p-6">
|
||||
<div class="flex items-center gap-6">
|
||||
<Icon
|
||||
icon={extension.icons.light
|
||||
|
@ -141,12 +153,11 @@
|
|||
|
||||
<Separator />
|
||||
|
||||
{#if extension.metadata_count > 0}
|
||||
<Carousel.Root class="w-full">
|
||||
{#if screenshots.length > 0}
|
||||
<Carousel.Root>
|
||||
<Carousel.Content>
|
||||
{#each Array(extension.metadata_count) as _, i (i)}
|
||||
<Carousel.Item class="shrink grow-0 basis-auto">
|
||||
{@const imageUrl = `${extension.readme_assets_path}metadata/${extension.name}-${i + 1}.png`}
|
||||
{#each screenshots as imageUrl, i (imageUrl)}
|
||||
<Carousel.Item class="grow-0 basis-auto">
|
||||
<button class="w-full cursor-pointer" onclick={() => onOpenLightbox(imageUrl)}>
|
||||
<img
|
||||
src={imageUrl}
|
||||
|
@ -158,8 +169,8 @@
|
|||
</Carousel.Item>
|
||||
{/each}
|
||||
</Carousel.Content>
|
||||
<Carousel.Previous />
|
||||
<Carousel.Next />
|
||||
<Carousel.Previous class="-left-4" variant="default" />
|
||||
<Carousel.Next class="-right-4" variant="default" />
|
||||
</Carousel.Root>
|
||||
{/if}
|
||||
|
||||
|
@ -227,7 +238,7 @@
|
|||
>
|
||||
<Icon
|
||||
icon={contributor.avatar
|
||||
? { source: contributor.avatar, mask: 'Circle' }
|
||||
? { source: contributor.avatar, mask: 'circle' }
|
||||
: undefined}
|
||||
class="size-6"
|
||||
/>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<script lang="ts">
|
||||
import Icon from '../Icon.svelte';
|
||||
import { Download } from '@lucide/svelte';
|
||||
import type { Datum } from '$lib/store';
|
||||
import type { Extension } from '$lib/store';
|
||||
import ListItemBase from '../nodes/shared/ListItemBase.svelte';
|
||||
|
||||
type Props = {
|
||||
ext: Datum;
|
||||
ext: Extension;
|
||||
isSelected: boolean;
|
||||
onclick?: () => void;
|
||||
};
|
||||
|
@ -16,7 +16,7 @@
|
|||
<ListItemBase
|
||||
title={ext.title}
|
||||
subtitle={ext.description}
|
||||
icon={ext.icons.light ? { source: ext.icons.light, mask: 'Circle' } : undefined}
|
||||
icon={ext.icons.light ? { source: ext.icons.light, mask: 'roundedRectangle' } : undefined}
|
||||
{isSelected}
|
||||
{onclick}
|
||||
>
|
||||
|
@ -29,7 +29,7 @@
|
|||
{ext.download_count.toLocaleString()}
|
||||
</div>
|
||||
<Icon
|
||||
icon={ext.author.avatar ? { source: ext.author.avatar, mask: 'Circle' } : undefined}
|
||||
icon={ext.author.avatar ? { source: ext.author.avatar, mask: 'circle' } : undefined}
|
||||
class="size-6"
|
||||
/>
|
||||
{/snippet}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<script lang="ts">
|
||||
import type { Datum } from '$lib/store';
|
||||
import type { Extension } from '$lib/store';
|
||||
import ExtensionListItem from './ExtensionListItem.svelte';
|
||||
import { extensionsStore } from './store.svelte';
|
||||
import BaseList from '$lib/components/BaseList.svelte';
|
||||
import type { VListHandle } from 'virtua/svelte';
|
||||
|
||||
type Props = {
|
||||
onSelect: (ext: Datum) => void;
|
||||
onSelect: (ext: Extension) => void;
|
||||
onScroll: (offset: number) => void;
|
||||
vlistInstance: VListHandle | null;
|
||||
};
|
||||
|
@ -16,7 +16,7 @@
|
|||
type DisplayItem = {
|
||||
id: string | number;
|
||||
itemType: 'header' | 'item';
|
||||
data: Datum | string;
|
||||
data: Extension | string;
|
||||
};
|
||||
|
||||
let currentItems = $state<DisplayItem[]>([]);
|
||||
|
@ -25,7 +25,7 @@
|
|||
const newItems: DisplayItem[] = [];
|
||||
const addedIds = new Set<string>();
|
||||
|
||||
const addItems = (exts: Datum[]) => {
|
||||
const addItems = (exts: Extension[]) => {
|
||||
for (const ext of exts) {
|
||||
if (!addedIds.has(ext.id)) {
|
||||
newItems.push({ id: ext.id, itemType: 'item', data: ext });
|
||||
|
@ -90,7 +90,7 @@
|
|||
{:else}
|
||||
<BaseList
|
||||
items={currentItems}
|
||||
onenter={(item) => onSelect(item.data as Datum)}
|
||||
onenter={(item) => onSelect(item.data as Extension)}
|
||||
bind:selectedIndex={extensionsStore.selectedIndex}
|
||||
isItemSelectable={(item) => item.itemType === 'item'}
|
||||
onscroll={onScroll}
|
||||
|
@ -102,7 +102,7 @@
|
|||
{item.data}
|
||||
</h3>
|
||||
{:else if item.itemType === 'item'}
|
||||
<ExtensionListItem ext={item.data as Datum} {isSelected} {onclick} />
|
||||
<ExtensionListItem ext={item.data as Extension} {isSelected} {onclick} />
|
||||
{/if}
|
||||
{/snippet}
|
||||
</BaseList>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { StoreListingsReturnTypeSchema, type Datum } from '$lib/store';
|
||||
import { PaginatedExtensionsResponseSchema, type Extension } from '$lib/store';
|
||||
import { fetch } from '@tauri-apps/plugin-http';
|
||||
|
||||
export class ExtensionsStore {
|
||||
extensions = $state<Datum[]>([]);
|
||||
searchResults = $state<Datum[]>([]);
|
||||
featuredExtensions = $state<Datum[]>([]);
|
||||
trendingExtensions = $state<Datum[]>([]);
|
||||
extensions = $state<Extension[]>([]);
|
||||
searchResults = $state<Extension[]>([]);
|
||||
featuredExtensions = $state<Extension[]>([]);
|
||||
trendingExtensions = $state<Extension[]>([]);
|
||||
|
||||
isLoading = $state(true);
|
||||
isSearching = $state(false);
|
||||
|
@ -73,7 +73,7 @@ export class ExtensionsStore {
|
|||
`https://backend.raycast.com/api/v1/store_listings/search?q=${encodeURIComponent(value)}&per_page=${this.perPage}`
|
||||
);
|
||||
if (!res.ok) throw new Error(`Search failed: ${res.status}`);
|
||||
const parsed = StoreListingsReturnTypeSchema.parse(await res.json());
|
||||
const parsed = PaginatedExtensionsResponseSchema.parse(await res.json());
|
||||
this.searchResults = parsed.data;
|
||||
this.selectedIndex = 0;
|
||||
} catch (e: unknown) {
|
||||
|
@ -101,18 +101,18 @@ export class ExtensionsStore {
|
|||
]);
|
||||
|
||||
if (!storeRes.ok) throw new Error(`Store fetch failed: ${storeRes.status}`);
|
||||
const storeParsed = StoreListingsReturnTypeSchema.parse(await storeRes.json());
|
||||
const storeParsed = PaginatedExtensionsResponseSchema.parse(await storeRes.json());
|
||||
this.extensions = storeParsed.data;
|
||||
this.currentPage = 1;
|
||||
this.hasMore = storeParsed.data.length === this.perPage;
|
||||
|
||||
if (featuredRes.ok) {
|
||||
const featuredParsed = StoreListingsReturnTypeSchema.parse(await featuredRes.json());
|
||||
const featuredParsed = PaginatedExtensionsResponseSchema.parse(await featuredRes.json());
|
||||
this.featuredExtensions = featuredParsed.data;
|
||||
}
|
||||
|
||||
if (trendingRes.ok) {
|
||||
const trendingParsed = StoreListingsReturnTypeSchema.parse(await trendingRes.json());
|
||||
const trendingParsed = PaginatedExtensionsResponseSchema.parse(await trendingRes.json());
|
||||
this.trendingExtensions = trendingParsed.data;
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
|
@ -141,7 +141,7 @@ export class ExtensionsStore {
|
|||
`https://backend.raycast.com/api/v1/store_listings?page=${nextPage}&per_page=${this.perPage}`
|
||||
);
|
||||
if (!res.ok) throw new Error('Failed to fetch more extensions');
|
||||
const parsed = StoreListingsReturnTypeSchema.parse(await res.json());
|
||||
const parsed = PaginatedExtensionsResponseSchema.parse(await res.json());
|
||||
|
||||
if (parsed.data.length < this.perPage) {
|
||||
this.hasMore = false;
|
||||
|
|
|
@ -30,7 +30,14 @@ export type Platform = z.infer<typeof PlatformSchema>;
|
|||
export const StatusSchema = z.enum(['active', 'deprecated']);
|
||||
export type Status = z.infer<typeof StatusSchema>;
|
||||
|
||||
export const IconsSchema = z.object({
|
||||
light: z.union([z.null(), z.string()]),
|
||||
dark: z.union([z.null(), z.string()])
|
||||
});
|
||||
export type Icons = z.infer<typeof IconsSchema>;
|
||||
|
||||
export const AuthorSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
name: z.string(),
|
||||
handle: z.string(),
|
||||
bio: z.union([z.null(), z.string()]).optional(),
|
||||
|
@ -41,21 +48,14 @@ export const AuthorSchema = z.object({
|
|||
avatar_placeholder_color: AvatarPlaceholderColorSchema,
|
||||
slack_community_username: z.union([z.null(), z.string()]).optional(),
|
||||
slack_community_user_id: z.union([z.null(), z.string()]).optional(),
|
||||
website_anchor: z.union([z.null(), z.string()]).optional(),
|
||||
created_at: z.number().optional(),
|
||||
website_anchor: z.union([z.null(), z.string()]).optional(),
|
||||
website: z.union([z.null(), z.string()]).optional(),
|
||||
username: z.string().optional(),
|
||||
avatar: z.union([z.null(), z.string()]),
|
||||
id: z.string().optional()
|
||||
avatar: z.union([z.null(), z.string()])
|
||||
});
|
||||
export type Author = z.infer<typeof AuthorSchema>;
|
||||
|
||||
export const IconsSchema = z.object({
|
||||
light: z.union([z.null(), z.string()]),
|
||||
dark: z.union([z.null(), z.string()])
|
||||
});
|
||||
export type Icons = z.infer<typeof IconsSchema>;
|
||||
|
||||
export const ToolSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
|
@ -83,21 +83,34 @@ export const CommandSchema = z.object({
|
|||
});
|
||||
export type Command = z.infer<typeof CommandSchema>;
|
||||
|
||||
export const DatumSchema = z.object({
|
||||
export const VersionSchema = z.object({
|
||||
title: z.string(),
|
||||
title_link: z.null(),
|
||||
date: z.string(),
|
||||
markdown: z.string()
|
||||
});
|
||||
export type Version = z.infer<typeof VersionSchema>;
|
||||
|
||||
export const ChangelogSchema = z.object({
|
||||
versions: z.array(VersionSchema)
|
||||
});
|
||||
export type Changelog = z.infer<typeof ChangelogSchema>;
|
||||
|
||||
export const ExtensionSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
native_id: z.null(),
|
||||
seo_categories: z.array(z.string()),
|
||||
platforms: z.union([z.array(PlatformSchema), z.null()]),
|
||||
created_at: z.number(),
|
||||
author: AuthorSchema,
|
||||
created_at: z.number(),
|
||||
kill_listed_at: z.number().nullable(),
|
||||
owner: AuthorSchema,
|
||||
status: StatusSchema,
|
||||
is_new: z.boolean(),
|
||||
access: AccessSchema,
|
||||
store_url: z.string(),
|
||||
download_count: z.number(),
|
||||
kill_listed_at: z.number().nullable(),
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
commit_sha: z.string(),
|
||||
|
@ -111,14 +124,18 @@ export const DatumSchema = z.object({
|
|||
readme_url: z.union([z.null(), z.string()]),
|
||||
readme_assets_path: z.string(),
|
||||
icons: IconsSchema,
|
||||
download_url: z.string(),
|
||||
commands: z.array(CommandSchema),
|
||||
tools: z.array(ToolSchema),
|
||||
download_url: z.string(),
|
||||
contributors: z.array(AuthorSchema),
|
||||
tools: z.array(ToolSchema)
|
||||
past_contributors: z.array(z.any()).optional(),
|
||||
listed: z.boolean().optional(),
|
||||
metadata: z.array(z.string()).optional(),
|
||||
changelog: ChangelogSchema.optional()
|
||||
});
|
||||
export type Datum = z.infer<typeof DatumSchema>;
|
||||
export type Extension = z.infer<typeof ExtensionSchema>;
|
||||
|
||||
export const StoreListingsReturnTypeSchema = z.object({
|
||||
data: z.array(DatumSchema)
|
||||
export const PaginatedExtensionsResponseSchema = z.object({
|
||||
data: z.array(ExtensionSchema)
|
||||
});
|
||||
export type StoreListingsReturnType = z.infer<typeof StoreListingsReturnTypeSchema>;
|
||||
export type PaginatedExtensionsResponse = z.infer<typeof PaginatedExtensionsResponseSchema>;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue