mirror of
https://github.com/langbot-app/LangBot.git
synced 2025-11-26 03:44:58 +08:00
feat:Add README display to installed plugins
This commit is contained in:
1865
web/package-lock.json
generated
1865
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -45,6 +45,7 @@
|
|||||||
"axios": "^1.12.0",
|
"axios": "^1.12.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"highlight.js": "^11.11.1",
|
||||||
"i18next": "^25.1.2",
|
"i18next": "^25.1.2",
|
||||||
"i18next-browser-languagedetector": "^8.1.0",
|
"i18next-browser-languagedetector": "^8.1.0",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
@@ -59,6 +60,8 @@
|
|||||||
"react-i18next": "^15.5.1",
|
"react-i18next": "^15.5.1",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-photo-view": "^1.2.7",
|
"react-photo-view": "^1.2.7",
|
||||||
|
"react-syntax-highlighter": "^16.1.0",
|
||||||
|
"rehype-raw": "^7.0.0",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"sonner": "^2.0.3",
|
"sonner": "^2.0.3",
|
||||||
"tailwind-merge": "^3.2.0",
|
"tailwind-merge": "^3.2.0",
|
||||||
@@ -72,6 +75,7 @@
|
|||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.2.4",
|
"eslint-config-next": "15.2.4",
|
||||||
"eslint-config-prettier": "^10.1.2",
|
"eslint-config-prettier": "^10.1.2",
|
||||||
@@ -81,5 +85,6 @@
|
|||||||
"tw-animate-css": "^1.2.9",
|
"tw-animate-css": "^1.2.9",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"typescript-eslint": "^8.31.1"
|
"typescript-eslint": "^8.31.1"
|
||||||
}
|
},
|
||||||
|
"packageManager": "pnpm@8.9.2+sha512.b9d35fe91b2a5854dadc43034a3e7b2e675fa4b56e20e8e09ef078fa553c18f8aed44051e7b36e8b8dd435f97eb0c44c4ff3b44fc7c6fa7d21e1fac17bbe661e"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
|
|||||||
import { PluginCardVO } from '@/app/home/plugins/components/plugin-installed/PluginCardVO';
|
import { PluginCardVO } from '@/app/home/plugins/components/plugin-installed/PluginCardVO';
|
||||||
import PluginCardComponent from '@/app/home/plugins/components/plugin-installed/plugin-card/PluginCardComponent';
|
import PluginCardComponent from '@/app/home/plugins/components/plugin-installed/plugin-card/PluginCardComponent';
|
||||||
import PluginForm from '@/app/home/plugins/components/plugin-installed/plugin-form/PluginForm';
|
import PluginForm from '@/app/home/plugins/components/plugin-installed/plugin-form/PluginForm';
|
||||||
|
import PluginReadme from '@/app/home/plugins/components/plugin-installed/plugin-readme/PluginReadme';
|
||||||
import styles from '@/app/home/plugins/plugins.module.css';
|
import styles from '@/app/home/plugins/plugins.module.css';
|
||||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
import {
|
import {
|
||||||
@@ -39,6 +40,8 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
|||||||
const [selectedPlugin, setSelectedPlugin] = useState<PluginCardVO | null>(
|
const [selectedPlugin, setSelectedPlugin] = useState<PluginCardVO | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
const [readmeModalOpen, setReadmeModalOpen] = useState<boolean>(false);
|
||||||
|
const [readmePlugin, setReadmePlugin] = useState<PluginCardVO | null>(null);
|
||||||
const [showOperationModal, setShowOperationModal] = useState(false);
|
const [showOperationModal, setShowOperationModal] = useState(false);
|
||||||
const [operationType, setOperationType] = useState<PluginOperationType>(
|
const [operationType, setOperationType] = useState<PluginOperationType>(
|
||||||
PluginOperationType.DELETE,
|
PluginOperationType.DELETE,
|
||||||
@@ -106,6 +109,11 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
|||||||
setModalOpen(true);
|
setModalOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleViewReadme(plugin: PluginCardVO) {
|
||||||
|
setReadmePlugin(plugin);
|
||||||
|
setReadmeModalOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
function handlePluginDelete(plugin: PluginCardVO) {
|
function handlePluginDelete(plugin: PluginCardVO) {
|
||||||
setTargetPlugin(plugin);
|
setTargetPlugin(plugin);
|
||||||
setOperationType(PluginOperationType.DELETE);
|
setOperationType(PluginOperationType.DELETE);
|
||||||
@@ -316,6 +324,25 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog open={readmeModalOpen} onOpenChange={setReadmeModalOpen}>
|
||||||
|
<DialogContent className="sm:max-w-[900px] max-w-[90vw] max-h-[85vh] p-0 flex flex-col">
|
||||||
|
<DialogHeader className="px-6 pt-6 pb-2 border-b">
|
||||||
|
<DialogTitle>
|
||||||
|
{readmePlugin &&
|
||||||
|
`${readmePlugin.author}/${readmePlugin.name} - ${t('plugins.readme')}`}
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
{readmePlugin && (
|
||||||
|
<PluginReadme
|
||||||
|
pluginAuthor={readmePlugin.author}
|
||||||
|
pluginName={readmePlugin.name}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{pluginList.map((vo, index) => {
|
{pluginList.map((vo, index) => {
|
||||||
return (
|
return (
|
||||||
<div key={index}>
|
<div key={index}>
|
||||||
@@ -324,6 +351,7 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
|||||||
onCardClick={() => handlePluginClick(vo)}
|
onCardClick={() => handlePluginClick(vo)}
|
||||||
onDeleteClick={() => handlePluginDelete(vo)}
|
onDeleteClick={() => handlePluginDelete(vo)}
|
||||||
onUpgradeClick={() => handlePluginUpdate(vo)}
|
onUpgradeClick={() => handlePluginUpdate(vo)}
|
||||||
|
onViewReadme={() => handleViewReadme(vo)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,15 @@ import { PluginCardVO } from '@/app/home/plugins/components/plugin-installed/Plu
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { BugIcon, ExternalLink, Ellipsis, Trash, ArrowUp } from 'lucide-react';
|
import {
|
||||||
|
BugIcon,
|
||||||
|
ExternalLink,
|
||||||
|
Ellipsis,
|
||||||
|
Trash,
|
||||||
|
ArrowUp,
|
||||||
|
Settings,
|
||||||
|
FileText,
|
||||||
|
} from 'lucide-react';
|
||||||
import { getCloudServiceClientSync } from '@/app/infra/http';
|
import { getCloudServiceClientSync } from '@/app/infra/http';
|
||||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -19,20 +27,28 @@ export default function PluginCardComponent({
|
|||||||
onCardClick,
|
onCardClick,
|
||||||
onDeleteClick,
|
onDeleteClick,
|
||||||
onUpgradeClick,
|
onUpgradeClick,
|
||||||
|
onViewReadme,
|
||||||
}: {
|
}: {
|
||||||
cardVO: PluginCardVO;
|
cardVO: PluginCardVO;
|
||||||
onCardClick: () => void;
|
onCardClick: () => void;
|
||||||
onDeleteClick: (cardVO: PluginCardVO) => void;
|
onDeleteClick: (cardVO: PluginCardVO) => void;
|
||||||
onUpgradeClick: (cardVO: PluginCardVO) => void;
|
onUpgradeClick: (cardVO: PluginCardVO) => void;
|
||||||
|
onViewReadme: (cardVO: PluginCardVO) => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="w-[100%] h-[10rem] bg-white rounded-[10px] shadow-[0px_2px_2px_0_rgba(0,0,0,0.2)] p-[1.2rem] cursor-pointer dark:bg-[#1f1f22]"
|
className="w-[100%] h-[10rem] bg-white rounded-[10px] shadow-[0px_2px_2px_0_rgba(0,0,0,0.2)] p-[1.2rem] cursor-pointer dark:bg-[#1f1f22] relative transition-all duration-300 hover:shadow-[0px_4px_12px_0_rgba(0,0,0,0.15)] hover:scale-[1.02]"
|
||||||
onClick={onCardClick}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
if (!dropdownOpen) {
|
||||||
|
setIsHovered(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="w-full h-full flex flex-row items-start justify-start gap-[1.2rem]">
|
<div className="w-full h-full flex flex-row items-start justify-start gap-[1.2rem]">
|
||||||
{/* <svg
|
{/* <svg
|
||||||
@@ -148,13 +164,24 @@ export default function PluginCardComponent({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col items-center justify-between h-full">
|
<div className="flex flex-col items-center justify-between h-full relative z-20">
|
||||||
<div className="flex items-center justify-center"></div>
|
<div className="flex items-center justify-center"></div>
|
||||||
|
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
|
<DropdownMenu
|
||||||
|
open={dropdownOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
setDropdownOpen(open);
|
||||||
|
if (!open) {
|
||||||
|
setIsHovered(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost">
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="bg-white dark:bg-[#1f1f22] hover:bg-gray-100 dark:hover:bg-[#2a2a2d]"
|
||||||
|
>
|
||||||
<Ellipsis className="w-4 h-4" />
|
<Ellipsis className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@@ -174,7 +201,7 @@ export default function PluginCardComponent({
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="flex flex-row items-center justify-start gap-[0.4rem] cursor-pointer"
|
className="flex flex-row items-center justify-start gap-[0.4rem] cursor-pointer text-red-600 focus:text-red-600"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onDeleteClick(cardVO);
|
onDeleteClick(cardVO);
|
||||||
@@ -189,6 +216,35 @@ export default function PluginCardComponent({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Hover overlay with action buttons */}
|
||||||
|
<div
|
||||||
|
className={`absolute inset-0 bg-gray-100/65 dark:bg-black/40 rounded-[10px] flex items-center justify-center gap-3 transition-all duration-300 z-10 ${isHovered ? 'opacity-100' : 'opacity-0 pointer-events-none'}`}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onViewReadme(cardVO);
|
||||||
|
}}
|
||||||
|
className={`bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 transition-all duration-300 ${isHovered ? 'translate-y-0 opacity-100' : 'translate-y-2 opacity-0'}`}
|
||||||
|
style={{ transitionDelay: isHovered ? '50ms' : '0ms' }}
|
||||||
|
>
|
||||||
|
<FileText className="w-4 h-4" />
|
||||||
|
{t('plugins.readme')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onCardClick();
|
||||||
|
}}
|
||||||
|
variant="outline"
|
||||||
|
className={`bg-white hover:bg-gray-100 text-gray-900 dark:bg-white dark:hover:bg-gray-100 dark:text-gray-900 px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 transition-all duration-300 ${isHovered ? 'translate-y-0 opacity-100' : 'translate-y-2 opacity-0'}`}
|
||||||
|
style={{ transitionDelay: isHovered ? '100ms' : '0ms' }}
|
||||||
|
>
|
||||||
|
<Settings className="w-4 h-4" />
|
||||||
|
{t('plugins.config')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ export default function PluginForm({
|
|||||||
itemConfigList={pluginInfo.manifest.manifest.spec.config}
|
itemConfigList={pluginInfo.manifest.manifest.spec.config}
|
||||||
initialValues={pluginConfig.config as Record<string, object>}
|
initialValues={pluginConfig.config as Record<string, object>}
|
||||||
onSubmit={(values) => {
|
onSubmit={(values) => {
|
||||||
// 只保存表单值的引用,不触发状态更新
|
// 只保存表单值的引用,不触发状态更新
|
||||||
currentFormValues.current = values;
|
currentFormValues.current = values;
|
||||||
}}
|
}}
|
||||||
onFileUploaded={(fileKey) => {
|
onFileUploaded={(fileKey) => {
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useTheme } from 'next-themes';
|
||||||
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
import remarkGfm from 'remark-gfm';
|
||||||
|
import rehypeRaw from 'rehype-raw';
|
||||||
|
import rehypeHighlight from 'rehype-highlight';
|
||||||
|
import rehypeSlug from 'rehype-slug';
|
||||||
|
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
|
||||||
|
import './github-markdown.css';
|
||||||
|
|
||||||
|
export default function PluginReadme({
|
||||||
|
pluginAuthor,
|
||||||
|
pluginName,
|
||||||
|
}: {
|
||||||
|
pluginAuthor: string;
|
||||||
|
pluginName: string;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { theme, systemTheme } = useTheme();
|
||||||
|
const [readme, setReadme] = useState<string>('');
|
||||||
|
const [isLoadingReadme, setIsLoadingReadme] = useState(false);
|
||||||
|
|
||||||
|
// Dynamically load highlight.js theme based on current theme
|
||||||
|
useEffect(() => {
|
||||||
|
const currentTheme = theme === 'system' ? systemTheme : theme;
|
||||||
|
const isDark = currentTheme === 'dark';
|
||||||
|
|
||||||
|
// Remove existing highlight.js theme
|
||||||
|
const existingTheme = document.querySelector('link[data-highlight-theme]');
|
||||||
|
if (existingTheme) {
|
||||||
|
existingTheme.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new theme
|
||||||
|
const link = document.createElement('link');
|
||||||
|
link.rel = 'stylesheet';
|
||||||
|
link.setAttribute('data-highlight-theme', 'true');
|
||||||
|
link.href = isDark
|
||||||
|
? 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css'
|
||||||
|
: 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css';
|
||||||
|
document.head.appendChild(link);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const themeLink = document.querySelector('link[data-highlight-theme]');
|
||||||
|
if (themeLink) {
|
||||||
|
themeLink.remove();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [theme, systemTheme]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Fetch plugin README
|
||||||
|
setIsLoadingReadme(true);
|
||||||
|
httpClient
|
||||||
|
.getPluginReadme(pluginAuthor, pluginName)
|
||||||
|
.then((res) => {
|
||||||
|
setReadme(res.readme);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setReadme('');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoadingReadme(false);
|
||||||
|
});
|
||||||
|
}, [pluginAuthor, pluginName]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full overflow-auto">
|
||||||
|
{isLoadingReadme ? (
|
||||||
|
<div className="p-6 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{t('plugins.loadingReadme')}
|
||||||
|
</div>
|
||||||
|
) : readme ? (
|
||||||
|
<div className="markdown-body p-6 max-w-none pt-0">
|
||||||
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkGfm]}
|
||||||
|
rehypePlugins={[
|
||||||
|
rehypeRaw,
|
||||||
|
rehypeHighlight,
|
||||||
|
rehypeSlug,
|
||||||
|
[
|
||||||
|
rehypeAutolinkHeadings,
|
||||||
|
{
|
||||||
|
behavior: 'wrap',
|
||||||
|
properties: {
|
||||||
|
className: ['anchor'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]}
|
||||||
|
components={{
|
||||||
|
ul: ({ children }) => <ul className="list-disc">{children}</ul>,
|
||||||
|
ol: ({ children }) => (
|
||||||
|
<ol className="list-decimal">{children}</ol>
|
||||||
|
),
|
||||||
|
li: ({ children }) => <li className="ml-4">{children}</li>,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{readme}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="p-6 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{t('plugins.noReadme')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,399 @@
|
|||||||
|
/* GitHub-style Markdown CSS */
|
||||||
|
.markdown-body {
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
color: var(--color-fg-default);
|
||||||
|
background-color: transparent;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide light theme highlight.js styles in dark mode */
|
||||||
|
.dark .markdown-body .hljs {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure code blocks have proper styling */
|
||||||
|
.markdown-body pre code.hljs {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .octicon {
|
||||||
|
display: inline-block;
|
||||||
|
fill: currentColor;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h1,
|
||||||
|
.markdown-body h2,
|
||||||
|
.markdown-body h3,
|
||||||
|
.markdown-body h4,
|
||||||
|
.markdown-body h5,
|
||||||
|
.markdown-body h6 {
|
||||||
|
margin-top: 24px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
padding-bottom: 0.3em;
|
||||||
|
border-bottom: 1px solid var(--color-border-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h2 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
padding-bottom: 0.3em;
|
||||||
|
border-bottom: 1px solid var(--color-border-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h3 {
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h4 {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h5 {
|
||||||
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h6 {
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: var(--color-fg-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body p {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body blockquote {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
padding: 0 1em;
|
||||||
|
color: var(--color-fg-muted);
|
||||||
|
border-left: 0.25em solid var(--color-border-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body ul,
|
||||||
|
.markdown-body ol {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body ul {
|
||||||
|
list-style-type: disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body ol {
|
||||||
|
list-style-type: decimal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body li {
|
||||||
|
margin-top: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body li + li {
|
||||||
|
margin-top: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body li > p {
|
||||||
|
margin-top: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body li > p:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body li > p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nested lists */
|
||||||
|
.markdown-body ul ul,
|
||||||
|
.markdown-body ul ol,
|
||||||
|
.markdown-body ol ol,
|
||||||
|
.markdown-body ol ul {
|
||||||
|
margin-top: 0.25em;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body ul ul {
|
||||||
|
list-style-type: circle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body ul ul ul {
|
||||||
|
list-style-type: square;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body code {
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 85%;
|
||||||
|
background-color: var(--color-neutral-muted);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body pre {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
overflow: auto;
|
||||||
|
font-size: 85%;
|
||||||
|
line-height: 1.45;
|
||||||
|
background-color: var(--color-canvas-subtle);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body pre code {
|
||||||
|
display: inline;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
overflow: visible;
|
||||||
|
line-height: inherit;
|
||||||
|
word-wrap: normal;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body a {
|
||||||
|
color: var(--color-accent-fg);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body table {
|
||||||
|
border-spacing: 0;
|
||||||
|
border-collapse: collapse;
|
||||||
|
display: block;
|
||||||
|
width: max-content;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body table tr {
|
||||||
|
background-color: transparent;
|
||||||
|
border-top: 1px solid var(--color-border-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body table tr:nth-child(2n) {
|
||||||
|
background-color: var(--color-canvas-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body table th,
|
||||||
|
.markdown-body table td {
|
||||||
|
padding: 6px 13px;
|
||||||
|
border: 1px solid var(--color-border-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body table th {
|
||||||
|
font-weight: 600;
|
||||||
|
background-color: var(--color-canvas-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body img {
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: content-box;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body hr {
|
||||||
|
height: 0.25em;
|
||||||
|
padding: 0;
|
||||||
|
margin: 24px 0;
|
||||||
|
background-color: var(--color-border-default);
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light theme colors */
|
||||||
|
.markdown-body {
|
||||||
|
--color-fg-default: #1f2328;
|
||||||
|
--color-fg-muted: #656d76;
|
||||||
|
--color-canvas-subtle: #f6f8fa;
|
||||||
|
--color-border-default: #d0d7de;
|
||||||
|
--color-border-muted: #d8dee4;
|
||||||
|
--color-neutral-muted: rgba(175, 184, 193, 0.2);
|
||||||
|
--color-accent-fg: #0969da;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark theme colors */
|
||||||
|
.dark .markdown-body {
|
||||||
|
--color-fg-default: #e6edf3;
|
||||||
|
--color-fg-muted: #8d96a0;
|
||||||
|
--color-canvas-subtle: #161b22;
|
||||||
|
--color-border-default: #30363d;
|
||||||
|
--color-border-muted: #21262d;
|
||||||
|
--color-neutral-muted: rgba(110, 118, 129, 0.4);
|
||||||
|
--color-accent-fg: #4493f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code highlighting styles */
|
||||||
|
.markdown-body .hljs {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-fg-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light theme syntax highlighting */
|
||||||
|
.markdown-body .hljs-comment,
|
||||||
|
.markdown-body .hljs-quote {
|
||||||
|
color: #6a737d;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .hljs-keyword,
|
||||||
|
.markdown-body .hljs-selector-tag,
|
||||||
|
.markdown-body .hljs-subst {
|
||||||
|
color: #d73a49;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .hljs-number,
|
||||||
|
.markdown-body .hljs-literal,
|
||||||
|
.markdown-body .hljs-variable,
|
||||||
|
.markdown-body .hljs-template-variable,
|
||||||
|
.markdown-body .hljs-tag .hljs-attr {
|
||||||
|
color: #005cc5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .hljs-string,
|
||||||
|
.markdown-body .hljs-doctag {
|
||||||
|
color: #032f62;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .hljs-title,
|
||||||
|
.markdown-body .hljs-section,
|
||||||
|
.markdown-body .hljs-selector-id {
|
||||||
|
color: #6f42c1;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .hljs-type,
|
||||||
|
.markdown-body .hljs-class .hljs-title {
|
||||||
|
color: #6f42c1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .hljs-tag,
|
||||||
|
.markdown-body .hljs-name,
|
||||||
|
.markdown-body .hljs-attribute {
|
||||||
|
color: #22863a;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .hljs-regexp,
|
||||||
|
.markdown-body .hljs-link {
|
||||||
|
color: #032f62;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .hljs-symbol,
|
||||||
|
.markdown-body .hljs-bullet {
|
||||||
|
color: #e36209;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .hljs-built_in,
|
||||||
|
.markdown-body .hljs-builtin-name {
|
||||||
|
color: #005cc5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .hljs-meta {
|
||||||
|
color: #6a737d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .hljs-deletion {
|
||||||
|
background-color: #ffeef0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .hljs-addition {
|
||||||
|
background-color: #e6ffed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .hljs-emphasis {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .hljs-strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark theme syntax highlighting */
|
||||||
|
.dark .markdown-body .hljs-comment,
|
||||||
|
.dark .markdown-body .hljs-quote {
|
||||||
|
color: #8b949e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .markdown-body .hljs-keyword,
|
||||||
|
.dark .markdown-body .hljs-selector-tag,
|
||||||
|
.dark .markdown-body .hljs-subst {
|
||||||
|
color: #ff7b72;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .markdown-body .hljs-number,
|
||||||
|
.dark .markdown-body .hljs-literal,
|
||||||
|
.dark .markdown-body .hljs-variable,
|
||||||
|
.dark .markdown-body .hljs-template-variable,
|
||||||
|
.dark .markdown-body .hljs-tag .hljs-attr {
|
||||||
|
color: #79c0ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .markdown-body .hljs-string,
|
||||||
|
.dark .markdown-body .hljs-doctag {
|
||||||
|
color: #a5d6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .markdown-body .hljs-title,
|
||||||
|
.dark .markdown-body .hljs-section,
|
||||||
|
.dark .markdown-body .hljs-selector-id {
|
||||||
|
color: #d2a8ff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .markdown-body .hljs-type,
|
||||||
|
.dark .markdown-body .hljs-class .hljs-title {
|
||||||
|
color: #d2a8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .markdown-body .hljs-tag,
|
||||||
|
.dark .markdown-body .hljs-name,
|
||||||
|
.dark .markdown-body .hljs-attribute {
|
||||||
|
color: #7ee787;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .markdown-body .hljs-regexp,
|
||||||
|
.dark .markdown-body .hljs-link {
|
||||||
|
color: #a5d6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .markdown-body .hljs-symbol,
|
||||||
|
.dark .markdown-body .hljs-bullet {
|
||||||
|
color: #ffa657;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .markdown-body .hljs-built_in,
|
||||||
|
.dark .markdown-body .hljs-builtin-name {
|
||||||
|
color: #79c0ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .markdown-body .hljs-meta {
|
||||||
|
color: #8b949e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .markdown-body .hljs-deletion {
|
||||||
|
background-color: rgba(248, 81, 73, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .markdown-body .hljs-addition {
|
||||||
|
background-color: rgba(46, 160, 67, 0.15);
|
||||||
|
}
|
||||||
@@ -48,7 +48,7 @@ export default function PluginMarketCardComponent({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="w-[100%] h-auto min-h-[8rem] sm:min-h-[9rem] bg-white rounded-[10px] shadow-[0px_0px_4px_0_rgba(0,0,0,0.2)] p-3 sm:p-[1rem] hover:shadow-[0px_2px_8px_0_rgba(0,0,0,0.15)] transition-shadow duration-200 dark:bg-[#1f1f22] relative"
|
className="w-[100%] h-auto min-h-[8rem] sm:min-h-[9rem] bg-white rounded-[10px] shadow-[0px_0px_4px_0_rgba(0,0,0,0.2)] p-3 sm:p-[1rem] hover:shadow-[0px_4px_12px_0_rgba(0,0,0,0.15)] transition-all duration-300 hover:scale-[1.02] dark:bg-[#1f1f22] relative"
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
>
|
>
|
||||||
@@ -137,25 +137,27 @@ export default function PluginMarketCardComponent({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Hover overlay with action buttons */}
|
{/* Hover overlay with action buttons */}
|
||||||
{isHovered && (
|
<div
|
||||||
<div className="absolute inset-0 bg-gray-100/65 dark:bg-black/40 rounded-[10px] flex items-center justify-center gap-3 transition-opacity duration-200">
|
className={`absolute inset-0 bg-gray-100/65 dark:bg-black/40 rounded-[10px] flex items-center justify-center gap-3 transition-all duration-300 ${isHovered ? 'opacity-100' : 'opacity-0 pointer-events-none'}`}
|
||||||
<Button
|
>
|
||||||
onClick={handleInstallClick}
|
<Button
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2"
|
onClick={handleInstallClick}
|
||||||
>
|
className={`bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 transition-all duration-300 ${isHovered ? 'translate-y-0 opacity-100' : 'translate-y-2 opacity-0'}`}
|
||||||
<Download className="w-4 h-4" />
|
style={{ transitionDelay: isHovered ? '50ms' : '0ms' }}
|
||||||
{t('market.install')}
|
>
|
||||||
</Button>
|
<Download className="w-4 h-4" />
|
||||||
<Button
|
{t('market.install')}
|
||||||
onClick={handleViewDetailsClick}
|
</Button>
|
||||||
variant="outline"
|
<Button
|
||||||
className="bg-white hover:bg-gray-100 text-gray-900 dark:bg-white dark:hover:bg-gray-100 dark:text-gray-900 px-4 py-2 rounded-lg shadow-lg flex items-center gap-2"
|
onClick={handleViewDetailsClick}
|
||||||
>
|
variant="outline"
|
||||||
<ExternalLink className="w-4 h-4" />
|
className={`bg-white hover:bg-gray-100 text-gray-900 dark:bg-white dark:hover:bg-gray-100 dark:text-gray-900 px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 transition-all duration-300 ${isHovered ? 'translate-y-0 opacity-100' : 'translate-y-2 opacity-0'}`}
|
||||||
{t('market.viewDetails')}
|
style={{ transitionDelay: isHovered ? '100ms' : '0ms' }}
|
||||||
</Button>
|
>
|
||||||
</div>
|
<ExternalLink className="w-4 h-4" />
|
||||||
)}
|
{t('market.viewDetails')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
padding-left: 0.8rem;
|
padding-left: 0.8rem;
|
||||||
padding-right: 0.8rem;
|
padding-right: 0.8rem;
|
||||||
padding-top: 2rem;
|
padding-top: 2rem;
|
||||||
padding-bottom: 2rem;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(30rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(30rem, 1fr));
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
|
|||||||
@@ -483,6 +483,13 @@ export class BackendClient extends BaseHttpClient {
|
|||||||
return this.delete(`/api/v1/plugins/config-files/${fileKey}`);
|
return this.delete(`/api/v1/plugins/config-files/${fileKey}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getPluginReadme(
|
||||||
|
author: string,
|
||||||
|
name: string,
|
||||||
|
): Promise<{ readme: string }> {
|
||||||
|
return this.get(`/api/v1/plugins/${author}/${name}/readme`);
|
||||||
|
}
|
||||||
|
|
||||||
public getPluginIconURL(author: string, name: string): string {
|
public getPluginIconURL(author: string, name: string): string {
|
||||||
if (this.instance.defaults.baseURL === '/') {
|
if (this.instance.defaults.baseURL === '/') {
|
||||||
const url = window.location.href;
|
const url = window.location.href;
|
||||||
|
|||||||
@@ -280,6 +280,10 @@ const enUS = {
|
|||||||
saveConfigSuccessDebugPlugin:
|
saveConfigSuccessDebugPlugin:
|
||||||
'Configuration saved successfully, please manually restart the plugin',
|
'Configuration saved successfully, please manually restart the plugin',
|
||||||
saveConfigError: 'Configuration save failed: ',
|
saveConfigError: 'Configuration save failed: ',
|
||||||
|
config: 'Configuration',
|
||||||
|
readme: 'Documentation',
|
||||||
|
loadingReadme: 'Loading documentation...',
|
||||||
|
noReadme: 'This plugin does not provide README documentation',
|
||||||
fileUpload: {
|
fileUpload: {
|
||||||
tooLarge: 'File size exceeds 10MB limit',
|
tooLarge: 'File size exceeds 10MB limit',
|
||||||
success: 'File uploaded successfully',
|
success: 'File uploaded successfully',
|
||||||
|
|||||||
@@ -281,6 +281,10 @@ const jaJP = {
|
|||||||
saveConfigSuccessDebugPlugin:
|
saveConfigSuccessDebugPlugin:
|
||||||
'設定を保存しました。手動でプラグインを再起動してください',
|
'設定を保存しました。手動でプラグインを再起動してください',
|
||||||
saveConfigError: '設定の保存に失敗しました:',
|
saveConfigError: '設定の保存に失敗しました:',
|
||||||
|
config: '設定',
|
||||||
|
readme: 'ドキュメント',
|
||||||
|
loadingReadme: 'ドキュメントを読み込み中...',
|
||||||
|
noReadme: 'このプラグインはREADMEドキュメントを提供していません',
|
||||||
fileUpload: {
|
fileUpload: {
|
||||||
tooLarge: 'ファイルサイズが 10MB の制限を超えています',
|
tooLarge: 'ファイルサイズが 10MB の制限を超えています',
|
||||||
success: 'ファイルのアップロードに成功しました',
|
success: 'ファイルのアップロードに成功しました',
|
||||||
|
|||||||
@@ -266,6 +266,10 @@ const zhHans = {
|
|||||||
saveConfigSuccessNormal: '保存配置成功',
|
saveConfigSuccessNormal: '保存配置成功',
|
||||||
saveConfigSuccessDebugPlugin: '保存配置成功,请手动重启插件',
|
saveConfigSuccessDebugPlugin: '保存配置成功,请手动重启插件',
|
||||||
saveConfigError: '保存配置失败:',
|
saveConfigError: '保存配置失败:',
|
||||||
|
config: '配置',
|
||||||
|
readme: '文档',
|
||||||
|
loadingReadme: '正在加载文档...',
|
||||||
|
noReadme: '该插件没有提供 README 文档',
|
||||||
fileUpload: {
|
fileUpload: {
|
||||||
tooLarge: '文件大小超过 10MB 限制',
|
tooLarge: '文件大小超过 10MB 限制',
|
||||||
success: '文件上传成功',
|
success: '文件上传成功',
|
||||||
|
|||||||
@@ -265,6 +265,10 @@ const zhHant = {
|
|||||||
saveConfigSuccessNormal: '儲存配置成功',
|
saveConfigSuccessNormal: '儲存配置成功',
|
||||||
saveConfigSuccessDebugPlugin: '儲存配置成功,請手動重啟插件',
|
saveConfigSuccessDebugPlugin: '儲存配置成功,請手動重啟插件',
|
||||||
saveConfigError: '儲存配置失敗:',
|
saveConfigError: '儲存配置失敗:',
|
||||||
|
config: '配置',
|
||||||
|
readme: '文件',
|
||||||
|
loadingReadme: '正在載入文件...',
|
||||||
|
noReadme: '該插件沒有提供 README 文件',
|
||||||
fileUpload: {
|
fileUpload: {
|
||||||
tooLarge: '檔案大小超過 10MB 限制',
|
tooLarge: '檔案大小超過 10MB 限制',
|
||||||
success: '檔案上傳成功',
|
success: '檔案上傳成功',
|
||||||
|
|||||||
Reference in New Issue
Block a user