mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-06 04:10:31 +08:00
Display images in source browser and diffs, enable preview for svgs
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { tick } from "svelte";
|
||||
|
||||
import ExpandButton from "./ExpandButton.svelte";
|
||||
|
||||
export let collapsable: boolean = false;
|
||||
@@ -74,7 +75,7 @@
|
||||
</div>
|
||||
|
||||
<div class="right">
|
||||
<slot name="right-header" />
|
||||
<slot name="right-header" {expanded} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -192,6 +192,16 @@ export function isCommit(input: string): boolean {
|
||||
return /^[a-f0-9]{40}$/.test(input);
|
||||
}
|
||||
|
||||
// Check whether the given path has a valid image file extension.
|
||||
export function isImagePath(input: string): boolean {
|
||||
return /\.(jpg|jpeg|png|gif|bmp)$/i.test(input);
|
||||
}
|
||||
|
||||
// Check whether the given path has a valid svg file extension.
|
||||
export function isSvgPath(input: string): boolean {
|
||||
return /\.svg$/i.test(input);
|
||||
}
|
||||
|
||||
export function isFulfilled<T>(
|
||||
input: PromiseSettledResult<T>,
|
||||
): input is PromiseFulfilledResult<T> {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { toHtml } from "hast-util-to-html";
|
||||
|
||||
import * as Syntax from "@app/lib/syntax";
|
||||
import { isImagePath, isSvgPath } from "@app/lib/utils";
|
||||
|
||||
import Badge from "@app/components/Badge.svelte";
|
||||
import File from "@app/components/File.svelte";
|
||||
@@ -14,6 +15,8 @@
|
||||
import Link from "@app/components/Link.svelte";
|
||||
import Loading from "@app/components/Loading.svelte";
|
||||
import Placeholder from "@app/components/Placeholder.svelte";
|
||||
import Radio from "@app/components/Radio.svelte";
|
||||
import Button from "@app/components/Button.svelte";
|
||||
|
||||
export let filePath: string;
|
||||
export let oldContent: string | undefined = undefined;
|
||||
@@ -35,6 +38,8 @@
|
||||
let selection: Selection | undefined = undefined;
|
||||
let highlighting: { new?: string[]; old?: string[] } | undefined = undefined;
|
||||
let syntaxHighlightingLoading: boolean = false;
|
||||
let preview = false;
|
||||
$: extension = filePath.split(".").pop();
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener("click", deselectHandler);
|
||||
@@ -400,29 +405,52 @@
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="right-header">
|
||||
<svelte:fragment slot="right-header" let:expanded>
|
||||
{#if revision}
|
||||
{#if syntaxHighlightingLoading}
|
||||
<Loading small />
|
||||
{/if}
|
||||
<Link
|
||||
route={{
|
||||
resource: "project.source",
|
||||
project: projectId,
|
||||
node: baseUrl,
|
||||
path: filePath,
|
||||
revision,
|
||||
}}>
|
||||
<IconButton title="View file">
|
||||
<IconSmall name="chevron-left-right" />
|
||||
</IconButton>
|
||||
</Link>
|
||||
<div style:display="flex" style:align-items="center" style:gap="0.5rem">
|
||||
{#if isSvgPath(filePath) && expanded}
|
||||
<Radio ariaLabel="Toggle render method">
|
||||
<Button
|
||||
styleBorderRadius="0"
|
||||
variant={!preview ? "selected" : "not-selected"}
|
||||
on:click={() => {
|
||||
preview = false;
|
||||
}}>
|
||||
<IconSmall name="chevron-left-right" />Code
|
||||
</Button>
|
||||
<Button
|
||||
styleBorderRadius="0"
|
||||
variant={preview ? "selected" : "not-selected"}
|
||||
on:click={() => {
|
||||
window.location.hash = "";
|
||||
preview = true;
|
||||
}}>
|
||||
<IconSmall name="eye-open" />Preview
|
||||
</Button>
|
||||
</Radio>
|
||||
{/if}
|
||||
<Link
|
||||
route={{
|
||||
resource: "project.source",
|
||||
project: projectId,
|
||||
node: baseUrl,
|
||||
path: filePath,
|
||||
revision,
|
||||
}}>
|
||||
<IconButton title="View file">
|
||||
<IconSmall name="chevron-left-right" />
|
||||
</IconButton>
|
||||
</Link>
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<div class="container">
|
||||
{#if fileDiff.type === "plain"}
|
||||
{#if fileDiff.hunks.length > 0}
|
||||
{#if fileDiff.hunks.length > 0 && !preview}
|
||||
<table class="diff" data-file-diff-select>
|
||||
{#each fileDiff.hunks as hunk, hunkIdx}
|
||||
<tr
|
||||
@@ -484,6 +512,18 @@
|
||||
{/each}
|
||||
{/each}
|
||||
</table>
|
||||
{:else if isImagePath(filePath) && extension && content}
|
||||
<div style:margin="1rem 0" style:text-align="center">
|
||||
<img
|
||||
src={`data:image/${extension};base64,${content}`}
|
||||
alt={filePath} />
|
||||
</div>
|
||||
{:else if preview && content}
|
||||
<div style:margin="1rem 0" style:text-align="center">
|
||||
<img
|
||||
src={`data:image/svg+xml;base64,${btoa(content)}`}
|
||||
alt={filePath} />
|
||||
</div>
|
||||
{:else}
|
||||
<div style:margin="1rem 0">
|
||||
<Placeholder iconName="empty-file" caption="Empty file" inline />
|
||||
|
||||
@@ -5,13 +5,14 @@
|
||||
import { toHtml } from "hast-util-to-html";
|
||||
|
||||
import * as Syntax from "@app/lib/syntax";
|
||||
import { isMarkdownPath } from "@app/lib/utils";
|
||||
import { isImagePath, isMarkdownPath, isSvgPath } from "@app/lib/utils";
|
||||
import { lineNumbersGutter } from "@app/lib/syntax";
|
||||
import { routeToPath } from "@app/lib/router";
|
||||
|
||||
import Button from "@app/components/Button.svelte";
|
||||
import File from "@app/components/File.svelte";
|
||||
import FilePath from "@app/components/FilePath.svelte";
|
||||
import IconSmall from "@app/components/IconSmall.svelte";
|
||||
import InlineMarkdown from "@app/components/InlineMarkdown.svelte";
|
||||
import Link from "@app/components/Link.svelte";
|
||||
import Markdown from "@app/components/Markdown.svelte";
|
||||
@@ -30,6 +31,7 @@
|
||||
$: lastCommit = blob.lastCommit;
|
||||
|
||||
$: content = highlighted ? lineNumbersGutter(highlighted) : undefined;
|
||||
$: extension = path.split(".").pop();
|
||||
|
||||
let selectedLineId: string | undefined = undefined;
|
||||
$: {
|
||||
@@ -47,7 +49,10 @@
|
||||
}
|
||||
|
||||
$: isMarkdown = isMarkdownPath(blob.path);
|
||||
$: showMarkdown = isMarkdown && selectedLineId === undefined;
|
||||
$: isImage = isImagePath(blob.path);
|
||||
$: isSvg = isSvgPath(blob.path);
|
||||
$: enablePreview = isMarkdown || isSvg;
|
||||
$: preview = enablePreview && selectedLineId === undefined;
|
||||
|
||||
let linkBaseUrl: string | undefined;
|
||||
|
||||
@@ -197,41 +202,59 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="global-hide-on-mobile teaser-buttons">
|
||||
{#if isMarkdown}
|
||||
{#if enablePreview}
|
||||
<Radio ariaLabel="Toggle render method">
|
||||
<Button
|
||||
styleBorderRadius="0"
|
||||
variant={showMarkdown ? "selected" : "not-selected"}
|
||||
variant={!preview ? "selected" : "not-selected"}
|
||||
on:click={() => {
|
||||
window.location.hash = "";
|
||||
showMarkdown = true;
|
||||
preview = false;
|
||||
}}>
|
||||
Markdown
|
||||
<IconSmall name="chevron-left-right" />Code
|
||||
</Button>
|
||||
<Button
|
||||
styleBorderRadius="0"
|
||||
variant={!showMarkdown ? "selected" : "not-selected"}
|
||||
variant={preview ? "selected" : "not-selected"}
|
||||
on:click={() => {
|
||||
showMarkdown = false;
|
||||
window.location.hash = "";
|
||||
preview = true;
|
||||
}}>
|
||||
Plain
|
||||
<IconSmall name="eye-open" />Preview
|
||||
</Button>
|
||||
</Radio>
|
||||
{/if}
|
||||
<a href="{rawPath}/{blob.path}">
|
||||
<Button variant="gray-white">Raw</Button>
|
||||
<a href="{rawPath}/{blob.path}" target="_blank" rel="noreferrer">
|
||||
<Button variant="gray-white">
|
||||
Raw <IconSmall name="arrow-box-up-right" />
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
||||
{#if blob.binary}
|
||||
<div style:margin="4rem 0" style:width="100%">
|
||||
<Placeholder iconName="binary-file" caption="Binary file" />
|
||||
</div>
|
||||
{:else if showMarkdown && blob.content}
|
||||
<div style:padding="2rem">
|
||||
<Markdown {linkBaseUrl} content={blob.content} {rawPath} {path} />
|
||||
</div>
|
||||
{#if blob.binary && blob.content}
|
||||
{#if isImage && extension}
|
||||
<div style:margin="1rem 0" style:text-align="center">
|
||||
<img
|
||||
src={`data:image/${extension};base64,${blob.content}`}
|
||||
alt={path} />
|
||||
</div>
|
||||
{:else}
|
||||
<div style:margin="4rem 0" style:width="100%">
|
||||
<Placeholder iconName="binary-file" caption="Binary file" />
|
||||
</div>
|
||||
{/if}
|
||||
{:else if preview && blob.content}
|
||||
{#if isMarkdown}
|
||||
<div style:padding="2rem">
|
||||
<Markdown {linkBaseUrl} content={blob.content} {rawPath} {path} />
|
||||
</div>
|
||||
{:else if isSvg}
|
||||
<div style:margin="1rem 0" style:text-align="center">
|
||||
<img
|
||||
src={`data:image/svg+xml;base64,${btoa(blob.content)}`}
|
||||
alt={path} />
|
||||
</div>
|
||||
{/if}
|
||||
{:else if content}
|
||||
<table class="code no-scrollbar">
|
||||
{@html toHtml(content)}
|
||||
|
||||
@@ -86,7 +86,7 @@ test("source file highlighting", async ({ page }) => {
|
||||
|
||||
test("navigate line numbers", async ({ page }) => {
|
||||
await page.goto(`${markdownUrl}/tree/main/cheatsheet.md`);
|
||||
await page.getByRole("button", { name: "Plain" }).click();
|
||||
await page.getByRole("button", { name: "Code" }).click();
|
||||
|
||||
await page.getByRole("link", { name: "5", exact: true }).click();
|
||||
await expect(page.locator("#L5")).toHaveClass("line highlight");
|
||||
@@ -103,7 +103,7 @@ test("navigate line numbers", async ({ page }) => {
|
||||
// Check that we go back to the Markdown view when navigating to a different
|
||||
// file.
|
||||
await page.getByRole("link", { name: "footnotes.md" }).click();
|
||||
await expect(page.getByRole("button", { name: "Markdown" })).toHaveClass(
|
||||
await expect(page.getByRole("button", { name: "Preview" })).toHaveClass(
|
||||
/selected/,
|
||||
);
|
||||
});
|
||||
@@ -229,21 +229,21 @@ test("markdown files", async ({ page }) => {
|
||||
|
||||
// Switch between raw and rendered modes.
|
||||
{
|
||||
await expect(page.getByRole("button", { name: "Markdown" })).toHaveClass(
|
||||
await expect(page.getByRole("button", { name: "Preview" })).toHaveClass(
|
||||
/selected/,
|
||||
);
|
||||
await expect(page.getByRole("button", { name: "Plain" })).toHaveClass(
|
||||
await expect(page.getByRole("button", { name: "Code" })).toHaveClass(
|
||||
/not-selected/,
|
||||
);
|
||||
await page.getByRole("button", { name: "Plain" }).click();
|
||||
await expect(page.getByRole("button", { name: "Markdown" })).toHaveClass(
|
||||
await page.getByRole("button", { name: "Code" }).click();
|
||||
await expect(page.getByRole("button", { name: "Preview" })).toHaveClass(
|
||||
/not-selected/,
|
||||
);
|
||||
await expect(page.getByRole("button", { name: "Plain" })).toHaveClass(
|
||||
await expect(page.getByRole("button", { name: "Code" })).toHaveClass(
|
||||
/selected/,
|
||||
);
|
||||
await expect(page.getByText("##### Table of Contents")).toBeVisible();
|
||||
await page.getByRole("button", { name: "Markdown" }).click();
|
||||
await page.getByRole("button", { name: "Preview" }).click();
|
||||
}
|
||||
|
||||
// Internal links go to anchor.
|
||||
|
||||
Reference in New Issue
Block a user