mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
feat(settings): Open Source Licenses section in System tab (#477)
* feat(licenses): tooling and initial data generation Adds the maintainer-only generator that produces src/data/licenses.json: - src-tauri/about.toml + about.hbs: cargo-about config + handlebars template for the Rust-side license enumeration - scripts/generate-licenses.mjs: orchestrator that runs cargo-about and license-checker-rseidelsohn (via npx, no devDep), merges the outputs into a single per-crate JSON with full license texts, and writes the result to src/data/licenses.json The script is invoked directly with `node scripts/generate-licenses.mjs` — no npm script wrapper on purpose, since adding one to package.json would trigger the nix-npm-deps-hash-sync workflow on every push. Initial generation covers 575 cargo crates + 71 npm packages (646 entries total, all with full license text bundled, ~1.4 MB JSON). * feat(licenses): Settings panel UI Adds a new Open Source Licenses section under Settings → System, sitting below Contributors. Components: - LicensesPanel.tsx: search input, curated highlight block of ~10 key dependencies (Tauri, React, rodio, symphonia, etc.), TanStack-Virtual list of all 600+ entries - LicenseTextModal.tsx: full-screen-ish modal showing the bundled license text plus name/version/license-id badges + repository link - licensesData.ts: lazy dynamic-import loader (Vite emits the JSON as a separate chunk, so the heavy ~1.4 MB payload is only loaded when the user actually opens the panel — no runtime fetch, the data is fixed into the build artifact) The panel registers itself in the Settings in-page search index under the System tab. * feat(licenses): i18n in 8 locales Adds the `licenses` namespace (title, intro, highlights, search placeholder, no-results, loading / load error, no-license-text, view-source, total line, generated-at) across en, de, fr, nl, zh, nb, ru, es. License names themselves (MIT, Apache-2.0, GPL-3.0, …) stay universal and are rendered as-is. * docs(release): document licenses regeneration step Adds a Step A.3 to the release SOP describing the maintainer-only `node scripts/generate-licenses.mjs` workflow, the cargo-about prerequisite, and explicitly notes why no npm script wrapper exists (would trigger the nix-npm-deps-hash-sync workflow).
This commit is contained in:
committed by
GitHub
parent
d48ea819c1
commit
8692e50603
@@ -0,0 +1,132 @@
|
||||
import { useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { X, ExternalLink } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { open as openExternal } from '@tauri-apps/plugin-shell';
|
||||
import type { LicenseEntry } from '../utils/licensesData';
|
||||
|
||||
interface Props {
|
||||
entry: LicenseEntry | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function LicenseTextModal({ entry, onClose }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (!entry) return;
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
};
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [entry, onClose]);
|
||||
|
||||
if (!entry) return null;
|
||||
|
||||
const repoLink = entry.repository || entry.homepage;
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
className="modal-overlay"
|
||||
onClick={onClose}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
style={{ alignItems: 'center', paddingTop: 0 }}
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
maxWidth: '760px',
|
||||
width: '92vw',
|
||||
maxHeight: '82vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<button className="modal-close" onClick={onClose} aria-label={t('common.close')}>
|
||||
<X size={18} />
|
||||
</button>
|
||||
<div style={{ marginBottom: '0.5rem', paddingRight: '2rem' }}>
|
||||
<h3 style={{ margin: 0, fontFamily: 'var(--font-display)', fontSize: '1.05rem' }}>
|
||||
{entry.name} <span style={{ color: 'var(--text-muted)', fontWeight: 400 }}>{entry.version}</span>
|
||||
</h3>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '8px',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
marginTop: '0.4rem',
|
||||
fontSize: '0.78rem',
|
||||
}}
|
||||
>
|
||||
{entry.licenses.map((lid) => (
|
||||
<span
|
||||
key={lid}
|
||||
style={{
|
||||
background: 'var(--bg-card)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: '10px',
|
||||
padding: '1px 8px',
|
||||
color: 'var(--text-primary)',
|
||||
}}
|
||||
>
|
||||
{lid}
|
||||
</span>
|
||||
))}
|
||||
<span
|
||||
style={{
|
||||
color: 'var(--text-muted)',
|
||||
textTransform: 'uppercase',
|
||||
fontSize: '0.7rem',
|
||||
letterSpacing: '0.04em',
|
||||
}}
|
||||
>
|
||||
{entry.source}
|
||||
</span>
|
||||
{repoLink && (
|
||||
<button
|
||||
onClick={() => openExternal(repoLink)}
|
||||
className="btn btn-ghost"
|
||||
style={{
|
||||
padding: '2px 8px',
|
||||
fontSize: '0.78rem',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
}}
|
||||
>
|
||||
<ExternalLink size={12} /> {t('licenses.viewSource')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{entry.description && (
|
||||
<p style={{ color: 'var(--text-muted)', marginTop: '0.6rem', marginBottom: 0, fontSize: '0.85rem' }}>
|
||||
{entry.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
background: 'var(--bg-card)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
padding: '12px 14px',
|
||||
overflow: 'auto',
|
||||
flex: 1,
|
||||
fontFamily: 'var(--font-mono, ui-monospace, monospace)',
|
||||
fontSize: '0.78rem',
|
||||
whiteSpace: 'pre-wrap',
|
||||
color: 'var(--text-primary)',
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
{entry.licenseText || t('licenses.noLicenseText')}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user