mirror of
https://github.com/misode/misode.github.io.git
synced 2026-04-26 08:26:51 +00:00
Add version mcmeta diff page (#428)
* Add version mcmeta diff page * Add toggle for word wrapping * Fix diff view on mobile * Use full layout width on version details * Show image and audio diffs * Add word_wrap locale
This commit is contained in:
@@ -1,48 +1,31 @@
|
||||
import { useMemo, useState } from 'preact/hooks'
|
||||
import { useFocus } from '../hooks/index.js'
|
||||
import { Octicon } from './index.js'
|
||||
|
||||
const SEPARATOR = '/'
|
||||
export type TreeViewGroupRenderer = (props: { name: string, open: boolean, onClick: () => void }) => JSX.Element
|
||||
export type TreeViewLeafRenderer<E> = (props: { entry: E }) => JSX.Element
|
||||
|
||||
export interface EntryAction {
|
||||
icon: keyof typeof Octicon,
|
||||
label: string,
|
||||
onAction: (entry: string) => unknown,
|
||||
interface Props<E> {
|
||||
entries: E[],
|
||||
split: (entry: E) => string[],
|
||||
group: TreeViewGroupRenderer,
|
||||
leaf: TreeViewLeafRenderer<E>,
|
||||
level?: number,
|
||||
}
|
||||
|
||||
export interface EntryError {
|
||||
path: string,
|
||||
message: string,
|
||||
}
|
||||
|
||||
interface Props {
|
||||
entries: string[],
|
||||
onSelect: (entry: string) => unknown,
|
||||
selected?: string,
|
||||
actions?: EntryAction[],
|
||||
errors?: EntryError[],
|
||||
indent?: number,
|
||||
}
|
||||
export function TreeView({ entries, onSelect, selected, actions, errors, indent }: Props) {
|
||||
export function TreeView<E>({ entries, split, group: Group, leaf: Leaf, level = 0 }: Props<E>) {
|
||||
const roots = useMemo(() => {
|
||||
const groups: Record<string, string[]> = {}
|
||||
const groups: Record<string, E[]> = {}
|
||||
for (const entry of entries) {
|
||||
const i = entry.indexOf(SEPARATOR)
|
||||
if (i >= 0) {
|
||||
const root = entry.slice(0, i)
|
||||
;(groups[root] ??= []).push(entry.slice(i + 1))
|
||||
const path = split(entry)
|
||||
if (path[level + 1] !== undefined) {
|
||||
;(groups[path[level]] ??= []).push(entry)
|
||||
}
|
||||
}
|
||||
return Object.entries(groups).map(([r, entries]) => {
|
||||
const rootActions = actions?.map(a => ({ ...a, onAction: (e: string) => a.onAction(r + SEPARATOR + e) }))
|
||||
const rootErrors = errors?.flatMap(e => e.path.startsWith(r + SEPARATOR) ? [{ ...e, path: e.path.slice(r.length + SEPARATOR.length) }] : [])
|
||||
return [r, entries, rootActions, rootErrors] as [string, string[], EntryAction[], EntryError[]]
|
||||
}).sort()
|
||||
}, [entries, actions, errors])
|
||||
return groups
|
||||
}, [entries, split, level])
|
||||
|
||||
const leaves = useMemo(() => {
|
||||
return entries.filter(e => !e.includes(SEPARATOR))
|
||||
}, [entries])
|
||||
return entries.filter(e => split(e).length === level + 1)
|
||||
}, [entries, split, level])
|
||||
|
||||
const [hidden, setHidden] = useState(new Set<string>())
|
||||
const toggle = (root: string) => {
|
||||
@@ -54,43 +37,12 @@ export function TreeView({ entries, onSelect, selected, actions, errors, indent
|
||||
setHidden(new Set(hidden))
|
||||
}
|
||||
|
||||
return <div class="tree-view" style={`--indent: ${indent ?? 0};`}>
|
||||
{roots.map(([r, entries, actions, errors]) => <div>
|
||||
<TreeViewEntry icon={hidden.has(r) ? 'chevron_right' : 'chevron_down'} key={r} label={r} onClick={() => toggle(r)} error={(errors?.length ?? 0) > 0} />
|
||||
return <div class="tree-view" style={`--indent: ${level};`}>
|
||||
{Object.entries(roots).map(([r, childs]) => <>
|
||||
<Group name={r} open={!hidden.has(r)} onClick={() => toggle(r)} />
|
||||
{!hidden.has(r) &&
|
||||
<TreeView entries={entries} onSelect={e => onSelect(`${r}${SEPARATOR}${e}`)}
|
||||
selected={selected?.startsWith(r + SEPARATOR) ? selected.substring(r.length + 1) : undefined}
|
||||
actions={actions} errors={errors} indent={(indent ?? 0) + 1} />}
|
||||
</div>)}
|
||||
{leaves.map(e => <TreeViewEntry icon="file" key={e} label={e} active={e === selected} onClick={() => onSelect(e)} actions={actions?.map(a => ({ ...a, onAction: () => a.onAction(e) }))} error={errors?.find(er => er.path === e)?.message} />)}
|
||||
</div>
|
||||
}
|
||||
|
||||
interface TreeViewEntryProps {
|
||||
icon: keyof typeof Octicon,
|
||||
label: string,
|
||||
active?: boolean,
|
||||
onClick?: () => unknown,
|
||||
actions?: EntryAction[],
|
||||
error?: string | boolean,
|
||||
}
|
||||
function TreeViewEntry({ icon, label, active, onClick, actions, error }: TreeViewEntryProps) {
|
||||
const [focused, setFocus] = useFocus()
|
||||
const onContextMenu = (evt: MouseEvent) => {
|
||||
evt.preventDefault()
|
||||
if (actions?.length) {
|
||||
setFocus()
|
||||
}
|
||||
}
|
||||
|
||||
return <div class={`entry${error ? ' has-error' : ''}${active ? ' active' : ''}${focused ? ' focused' : ''}`} onClick={onClick} onContextMenu={onContextMenu} >
|
||||
{Octicon[icon]}
|
||||
<span>{label.replaceAll('\u2215', '/')}</span>
|
||||
{typeof error === 'string' && <div class="status-icon danger tooltipped tip-se" aria-label={error}>
|
||||
{Octicon.issue_opened}
|
||||
</div>}
|
||||
{focused && <div class="entry-menu">
|
||||
{actions?.map(a => <div class="action" onClick={e => { a.onAction(''); e.stopPropagation(); setFocus(false) }}>{Octicon[a.icon]}{a.label}</div>)}
|
||||
</div>}
|
||||
<TreeView<E> entries={childs} split={split} group={Group} leaf={Leaf} level={level + 1} />}
|
||||
</>)}
|
||||
{leaves.map(e => <Leaf key={split(e).join('/')} entry={e} />)}
|
||||
</div>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user