From ddd00dd73125476cfe77ca1a38580ccd1b0d7087 Mon Sep 17 00:00:00 2001 From: Misode Date: Mon, 9 Oct 2023 22:09:36 +0200 Subject: [PATCH] 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 --- src/app/Utils.ts | 36 ++++ src/app/components/ErrorPanel.tsx | 2 +- src/app/components/Octicon.tsx | 4 + src/app/components/TreeView.tsx | 94 +++-------- src/app/components/generator/ProjectPanel.tsx | 48 ++++-- src/app/components/versions/IssueList.tsx | 3 +- src/app/components/versions/VersionDetail.tsx | 15 +- src/app/components/versions/VersionDiff.tsx | 155 ++++++++++++++++++ src/app/components/versions/index.ts | 1 + src/app/hooks/useSearchParam.ts | 2 +- src/app/pages/Versions.tsx | 30 ++-- src/app/services/DataFetcher.ts | 41 ++++- src/locales/en.json | 5 +- src/locales/fr.json | 2 +- src/locales/ja.json | 2 +- src/locales/ko.json | 2 +- src/locales/ru.json | 2 +- src/locales/zh-cn.json | 2 +- src/styles/global.css | 147 +++++++++++++++++ 19 files changed, 474 insertions(+), 119 deletions(-) create mode 100644 src/app/components/versions/VersionDiff.tsx diff --git a/src/app/Utils.ts b/src/app/Utils.ts index 8d4e6216..089b7039 100644 --- a/src/app/Utils.ts +++ b/src/app/Utils.ts @@ -545,3 +545,39 @@ export function composeMatrix(translation: Vector, leftRotation: quat, scale: Ve .scale(scale) .mul(Matrix4.fromQuat(rightRotation)) } + +export interface PatchLine { + line: string + before?: number + after?: number +} + +export function parseGitPatch(patch: string) { + const source = patch.split('\n') + const result: PatchLine[] = [] + let before = 1 + let after = 1 + for (let i = 0; i < source.length; i += 1) { + const line = source[i] + if (line.startsWith('@')) { + const match = line.match(/^@@ -(\d+)(?:,(?:\d+))? \+(\d+)(?:,(?:\d+))? @@/) + if (!match) throw new Error(`Invalid patch pattern at line ${i+1}: ${line}`) + result.push({ line }) + before = Number(match[1]) + after = Number(match[2]) + } else if (line.startsWith(' ')) { + result.push({ line, before, after }) + before += 1 + after += 1 + } else if (line.startsWith('+')) { + result.push({ line, after }) + after += 1 + } else if (line.startsWith('-')) { + result.push({ line, before }) + before += 1 + } else if (!line.startsWith('\\')) { + throw new Error(`Invalid patch, got ${line.charAt(0)} at line ${i+1}`) + } + } + return result +} diff --git a/src/app/components/ErrorPanel.tsx b/src/app/components/ErrorPanel.tsx index 065459a6..c6bb8d03 100644 --- a/src/app/components/ErrorPanel.tsx +++ b/src/app/components/ErrorPanel.tsx @@ -67,7 +67,7 @@ export function ErrorPanel({ error, prefix, reportable, onDismiss, body: body_, return
{onDismiss &&
{Octicon.x}
} -

+

{(prefix ?? '') + (error instanceof Error ? error.message : error)} {stack && setStackVisible(!stackVisible)}> {Octicon.info} diff --git a/src/app/components/Octicon.tsx b/src/app/components/Octicon.tsx index 4824ab60..fc7916df 100644 --- a/src/app/components/Octicon.tsx +++ b/src/app/components/Octicon.tsx @@ -13,6 +13,10 @@ export const Octicon = { clippy: , code: , codescan_checkmark: , + diff_added: , + diff_modified: , + diff_removed: , + diff_renamed: , dash: , device_desktop: , dot_fill: , diff --git a/src/app/components/TreeView.tsx b/src/app/components/TreeView.tsx index 498942a5..7ac7cf32 100644 --- a/src/app/components/TreeView.tsx +++ b/src/app/components/TreeView.tsx @@ -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 = (props: { entry: E }) => JSX.Element -export interface EntryAction { - icon: keyof typeof Octicon, - label: string, - onAction: (entry: string) => unknown, +interface Props { + entries: E[], + split: (entry: E) => string[], + group: TreeViewGroupRenderer, + leaf: TreeViewLeafRenderer, + 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({ entries, split, group: Group, leaf: Leaf, level = 0 }: Props) { const roots = useMemo(() => { - const groups: Record = {} + const groups: Record = {} 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()) const toggle = (root: string) => { @@ -54,43 +37,12 @@ export function TreeView({ entries, onSelect, selected, actions, errors, indent setHidden(new Set(hidden)) } - return
- {roots.map(([r, entries, actions, errors]) =>
- toggle(r)} error={(errors?.length ?? 0) > 0} /> + return
+ {Object.entries(roots).map(([r, childs]) => <> + toggle(r)} /> {!hidden.has(r) && - onSelect(`${r}${SEPARATOR}${e}`)} - selected={selected?.startsWith(r + SEPARATOR) ? selected.substring(r.length + 1) : undefined} - actions={actions} errors={errors} indent={(indent ?? 0) + 1} />} -
)} - {leaves.map(e => onSelect(e)} actions={actions?.map(a => ({ ...a, onAction: () => a.onAction(e) }))} error={errors?.find(er => er.path === e)?.message} />)} -
-} - -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
- {Octicon[icon]} - {label.replaceAll('\u2215', '/')} - {typeof error === 'string' &&
- {Octicon.issue_opened} -
} - {focused &&
- {actions?.map(a =>
{ a.onAction(''); e.stopPropagation(); setFocus(false) }}>{Octicon[a.icon]}{a.label}
)} -
} + entries={childs} split={split} group={Group} leaf={Leaf} level={level + 1} />} + )} + {leaves.map(e => )}
} diff --git a/src/app/components/generator/ProjectPanel.tsx b/src/app/components/generator/ProjectPanel.tsx index 8c22e1be..390e00a4 100644 --- a/src/app/components/generator/ProjectPanel.tsx +++ b/src/app/components/generator/ProjectPanel.tsx @@ -2,14 +2,16 @@ import type { DataModel } from '@mcschema/core' import { useCallback, useMemo, useRef, useState } from 'preact/hooks' import { Analytics } from '../../Analytics.js' import config from '../../Config.js' -import { disectFilePath, DRAFT_PROJECT, getFilePath, useLocale, useProject, useVersion } from '../../contexts/index.js' -import type { VersionId } from '../../services/index.js' -import { stringifySource } from '../../services/index.js' import { Store } from '../../Store.js' import { writeZip } from '../../Utils.js' +import { DRAFT_PROJECT, disectFilePath, getFilePath, useLocale, useProject, useVersion } from '../../contexts/index.js' +import { useFocus } from '../../hooks/useFocus.js' +import type { VersionId } from '../../services/index.js' +import { stringifySource } from '../../services/index.js' import { Btn } from '../Btn.js' import { BtnMenu } from '../BtnMenu.js' -import type { EntryAction } from '../TreeView.js' +import { Octicon } from '../Octicon.jsx' +import type { TreeViewGroupRenderer, TreeViewLeafRenderer } from '../TreeView.js' import { TreeView } from '../TreeView.js' interface Props { @@ -85,12 +87,12 @@ export function ProjectPanel({ onRename, onCreate, onDeleteProject }: Props) { download.current.click() } - const actions = useMemo(() => [ + const actions = useMemo(() => [ { icon: 'pencil', label: locale('project.rename_file'), - onAction: (e) => { - const file = disectEntry(e) + onAction: (entry: string) => { + const file = disectEntry(entry) if (file) { onRename(file) } @@ -99,8 +101,8 @@ export function ProjectPanel({ onRename, onCreate, onDeleteProject }: Props) { { icon: 'trashcan', label: locale('project.delete_file'), - onAction: (e) => { - const file = disectEntry(e) + onAction: (entry: string) => { + const file = disectEntry(entry) if (file) { Analytics.deleteProjectFile(file.type, projects.length, project.files.length, 'menu') updateFile(file.type, file.id, {}) @@ -109,6 +111,32 @@ export function ProjectPanel({ onRename, onCreate, onDeleteProject }: Props) { }, ], [disectEntry, updateFile, onRename]) + const FolderEntry: TreeViewGroupRenderer = useCallback(({ name, open, onClick }) => { + return
+ {Octicon[!open ? 'chevron_right' : 'chevron_down']} + {name} +
+ }, []) + + const FileEntry: TreeViewLeafRenderer = useCallback(({ entry }) => { + const [focused, setFocus] = useFocus() + const onContextMenu = (evt: MouseEvent) => { + evt.preventDefault() + setFocus() + } + + return
selectFile(entry)} onContextMenu={onContextMenu} > + {Octicon.file} + {entry.split('/').at(-1)} + {focused &&
+ {actions?.map(a =>
{ a.onAction(entry); e.stopPropagation(); setFocus(false) }}> + {(Octicon as any)[a.icon]} + {a.label} +
)} +
} +
+ }, [actions]) + return <>
@@ -124,7 +152,7 @@ export function ProjectPanel({ onRename, onCreate, onDeleteProject }: Props) {
{entries.length === 0 ? {locale('project.no_files')} - : } + : path.split('/')} group={FolderEntry} leaf={FileEntry} />}
diff --git a/src/app/components/versions/IssueList.tsx b/src/app/components/versions/IssueList.tsx index 2b49082f..3a42b2dd 100644 --- a/src/app/components/versions/IssueList.tsx +++ b/src/app/components/versions/IssueList.tsx @@ -1,7 +1,6 @@ import { useLocale } from '../../contexts/Locale.jsx' import { useAsync } from '../../hooks/useAsync.js' import { fetchBugfixes } from '../../services/DataFetcher.js' -import type { VersionId } from '../../services/Schemas.js' import { Issue } from './Issue.jsx' interface Props { @@ -9,7 +8,7 @@ interface Props { } export function IssueList({ version }: Props) { const { locale } = useLocale() - const { value: issues, loading } = useAsync(() => fetchBugfixes(version as VersionId), [version]) + const { value: issues, loading } = useAsync(() => fetchBugfixes(version), [version]) return
{issues === undefined || loading ? <> diff --git a/src/app/components/versions/VersionDetail.tsx b/src/app/components/versions/VersionDetail.tsx index 3681e548..50ad58ca 100644 --- a/src/app/components/versions/VersionDetail.tsx +++ b/src/app/components/versions/VersionDetail.tsx @@ -1,15 +1,14 @@ +import { Link } from 'preact-router' import { useEffect, useMemo } from 'preact/hooks' import { useLocale } from '../../contexts/index.js' import { useAsync } from '../../hooks/useAsync.js' import { useSearchParam } from '../../hooks/useSearchParam.js' import type { VersionMeta } from '../../services/index.js' import { fetchChangelogs, getArticleLink } from '../../services/index.js' -import { Giscus } from '../Giscus.js' import { Octicon } from '../Octicon.js' -import { ChangelogList } from './ChangelogList.js' -import { IssueList, VersionMetaData } from './index.js' +import { ChangelogList, IssueList, VersionDiff, VersionMetaData } from './index.js' -const Tabs = ['changelog', 'discussion', 'fixes'] +const Tabs = ['changelog', 'diff', 'fixes'] interface Props { id: string, @@ -51,9 +50,9 @@ export function VersionDetail({ id, version }: Props) {

}
- setTab('changelog')}>{locale('versions.technical_changes')} - setTab('discussion')}>{locale('versions.discussion')} - setTab('fixes')}>{locale('versions.fixes')} + {Tabs.map(t => + {locale(`versions.${t}`)} + )} {articleLink && {locale('versions.article')} {Octicon.link_external} @@ -61,7 +60,7 @@ export function VersionDetail({ id, version }: Props) {
{tab === 'changelog' && } - {tab === 'discussion' && } + {tab === 'diff' && } {tab === 'fixes' && }
diff --git a/src/app/components/versions/VersionDiff.tsx b/src/app/components/versions/VersionDiff.tsx new file mode 100644 index 00000000..ca18ea90 --- /dev/null +++ b/src/app/components/versions/VersionDiff.tsx @@ -0,0 +1,155 @@ +import { useCallback, useEffect, useMemo, useRef } from "preact/hooks" +import { parseGitPatch } from "../../Utils.js" +import { useLocale } from "../../contexts/Locale.jsx" +import { useAsync } from "../../hooks/useAsync.js" +import { useLocalStorage } from "../../hooks/useLocalStorage.js" +import { useSearchParam } from "../../hooks/useSearchParam.js" +import { GitHubCommitFile, fetchVersionDiff } from "../../services/DataFetcher.js" +import { ErrorPanel } from "../ErrorPanel.jsx" +import { Octicon } from "../Octicon.jsx" +import { TreeView, TreeViewGroupRenderer, TreeViewLeafRenderer } from "../TreeView.jsx" + +const mcmetaRawUrl = 'https://raw.githubusercontent.com/misode/mcmeta' +const mcmetaBlobUrl = 'https://github.com/misode/mcmeta/blob' + +interface Props { + version: string, +} +export function VersionDiff({ version }: Props) { + const { locale } = useLocale() + const { value: commit } = useAsync(() => fetchVersionDiff(version), [version]) + + const diffView = useRef(null) + + const [filename, setFilename] = useSearchParam('file') + const selectFile = useCallback((filename: string) => { + setFilename(filename) + if (diffView.current) { + const y = diffView.current.getBoundingClientRect().top + window.scrollY - 56 + window.scrollTo({ top: y, behavior: 'smooth' }) + } + }, [diffView, setFilename]) + + const { file, diff } = useMemo(() => { + if (filename === undefined) return { file: undefined, diff: undefined } + const file = commit?.files.find(f => f.filename === filename) + if (file === undefined) return { file, diff: undefined } + if (file.patch === undefined) { + const match = filename.match(/\.(png|ogg)$/) + if (match) { + return { + file, + diff: { + type: match[1], + before: file.status === 'added' ? undefined : `${mcmetaRawUrl}/${commit?.parents[0].sha}/${filename}`, + after: file.status === 'removed' ? undefined : `${mcmetaRawUrl}/${version}-diff/${filename}`, + }, + } + } else if (file.status === 'renamed') { + return { file, diff: [] } + } else { + return { file, diff: new Error('Cannot display diff for this file') } + } + } + try { + return { file, diff: parseGitPatch(file.patch) } + } catch (e) { + const error = e as Error + error.message = `Failed to show diff: ${error.message}` + return { file, diff: error } + } + }, [filename, commit]) + + const DiffFolder: TreeViewGroupRenderer = useCallback(({ name, open, onClick }) => { + return
+ {Octicon[!open ? 'chevron_right' : 'chevron_down']} + {name} +
+ }, []) + + const DiffEntry: TreeViewLeafRenderer = useCallback(({ entry }) => { + return
svg]:shrink-0 select-none ${entry.filename === filename ? 'active' : ''}`} onClick={() => selectFile(entry.filename)} title={entry.filename}> + {entry.filename.split('/').at(-1)} + {Octicon[`diff_${entry.status}`]} +
+ }, [filename]) + + useEffect(() => { + if (commit === undefined || filename === undefined) return + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { + const fileIndex = commit.files.findIndex(f => f.filename === filename) + const newFileIndex = fileIndex + (e.key === 'ArrowDown' ? 1 : -1) + if (newFileIndex >= 0 && newFileIndex < commit.files.length) { + selectFile(commit.files[newFileIndex].filename) + } + e.preventDefault() + } + } + window.addEventListener('keydown', onKeyDown) + return () => window.removeEventListener('keydown', onKeyDown) + }, [commit, filename, selectFile]) + + const [wrap, setWrap] = useLocalStorage('misode_diff_word_wrap', true, (s) => s === 'true', (b) => b ? 'true' : 'false') + + return <> +
+ +

Showing {commit?.files.length} changed files with {commit?.stats.additions} additions and {commit?.stats.deletions} deletions

+
+ {Array.isArray(diff) && } +
+
+
+ file.filename.split('/')} /> +
+ {filename &&
+ + {diff === undefined ? ( + {locale('loading')} + ) : diff instanceof Error ? ( + + ) : !Array.isArray(diff) ? ( +
+ {diff.before ? ( + diff.type === 'png' + ? Before image + :
+ ) : <> + {file.previous_filename !== undefined &&
+ {file.previous_filename} + {filename} +
} +
+ + {diff.map(line => + + + + + )} +
{line.before ?? (line.after ? '' : '...')}{line.after ?? (line.before ? '' : '...')}{line.line.startsWith('@') ? '' : line.line.charAt(0)}{line.line.startsWith('@') ? line.line : line.line.slice(1)}
+
+ } +
} +
+ +} diff --git a/src/app/components/versions/index.ts b/src/app/components/versions/index.ts index 1ffd2e69..b6213b9b 100644 --- a/src/app/components/versions/index.ts +++ b/src/app/components/versions/index.ts @@ -3,6 +3,7 @@ export * from './ChangelogEntry.js' export * from './ChangelogList.js' export * from './IssueList.jsx' export * from './VersionDetail.js' +export * from './VersionDiff.jsx' export * from './VersionEntry.js' export * from './VersionList.js' export * from './VersionMetaData.js' diff --git a/src/app/hooks/useSearchParam.ts b/src/app/hooks/useSearchParam.ts index c84fb1fb..c37e3cd1 100644 --- a/src/app/hooks/useSearchParam.ts +++ b/src/app/hooks/useSearchParam.ts @@ -32,7 +32,7 @@ export function useSearchParam(param: string): [string | undefined, (value: stri } else { params.set(param, newValue) } - changeUrl({ search: params.toString().replaceAll('%7C', '|'), replace }) + changeUrl({ search: params.toString().replaceAll('%7C', '|').replaceAll('%2F', '/'), replace }) } }, [value]) diff --git a/src/app/pages/Versions.tsx b/src/app/pages/Versions.tsx index 24dc8d2f..1fa170f9 100644 --- a/src/app/pages/Versions.tsx +++ b/src/app/pages/Versions.tsx @@ -27,22 +27,20 @@ export function Versions({}: Props) { return
{error && } -
- {selectedId ? <> - - - : <> - `/versions/?id=${id}`} navigation={( - - )} /> - } -
+ {selectedId ?
+ + +
:
+ `/versions/?id=${id}`} navigation={( + + )} /> +
}
} diff --git a/src/app/services/DataFetcher.ts b/src/app/services/DataFetcher.ts index 81cba052..742fb7d7 100644 --- a/src/app/services/DataFetcher.ts +++ b/src/app/services/DataFetcher.ts @@ -20,7 +20,8 @@ const mcmetaUrl = 'https://raw.githubusercontent.com/misode/mcmeta' const mcmetaTarballUrl = 'https://github.com/misode/mcmeta/tarball' const changesUrl = 'https://raw.githubusercontent.com/misode/technical-changes' const fixesUrl = 'https://raw.githubusercontent.com/misode/mcfixes' -const whatsNewUrl = 'https://whats-new.misode.workers.dev/' +const versionDiffUrl = 'https://mcmeta-diff.misode.workers.dev' +const whatsNewUrl = 'https://whats-new.misode.workers.dev' type McmetaTypes = 'summary' | 'data' | 'data-json' | 'assets' | 'assets-json' | 'registries' | 'atlas' @@ -273,12 +274,46 @@ export interface Bugfix { votes: number, } -export async function fetchBugfixes(version: VersionId): Promise { +export async function fetchBugfixes(version: string): Promise { try { const fixes = await cachedFetch(`${fixesUrl}/main/versions/${version}.json`, { refresh: true }) return fixes } catch (e) { - throw new Error(`Error occured while fetching bugfixes: ${message(e)}`) + throw new Error(`Error occured while fetching bugfixes for version ${version}: ${message(e)}`) + } +} + +export interface GitHubCommitFile { + sha: string, + filename: string, + previous_filename?: string, + status: 'added' | 'modified' | 'removed' | 'renamed', + additions: number, + deletions: number, + changes: number, + patch: string, +} + +export interface GitHubCommit { + sha: string, + html_url: string, + parents: { + sha: string, + }[], + stats: { + total: number, + additions: number, + deletions: number, + }, + files: GitHubCommitFile[], +} + +export async function fetchVersionDiff(version: string) { + try { + const diff = await cachedFetch(`${versionDiffUrl}/${version}`, { refresh: true }) + return diff + } catch (e) { + throw new Error(`Error occured while fetching diff for version ${version}: ${message(e)}`) } } diff --git a/src/locales/en.json b/src/locales/en.json index ad68190c..e9ee53f6 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -247,13 +247,14 @@ "versions.pack_format": "Pack format", "versions.data_pack_format": "Data pack format", "versions.resource_pack_format": "Resource pack format", - "versions.technical_changes": "Technical changes", - "versions.discussion": "Discussion", + "versions.changelog": "Technical changes", + "versions.diff": "Mcmeta diff", "versions.fixes": "Fixed bugs", "versions.fixes.no_results": "No fixes", "versions.minecraft_versions": "Minecraft Versions", "versions.latest_snapshot": "Latest snapshot", "versions.latest_release": "Latest release", + "version_diff.word_wrap": "Word wrap", "weight": "Weight", "whats_new": "What's new?", "whats_new.description": "Stay informed about all the latest development on misode.github.io. Read below to find out which features have recently been added.", diff --git a/src/locales/fr.json b/src/locales/fr.json index 49bd80c8..c7474e32 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -201,7 +201,7 @@ "versions.released": "Sortie", "versions.resource_pack_format": "Format du pack de ressources", "versions.search": "Rechercher des versions", - "versions.technical_changes": "Modifications techniques", + "versions.changelog": "Modifications techniques", "world": "Paramètres du monde", "worldgen": "Générateur de monde", "worldgen/biome": "Biome", diff --git a/src/locales/ja.json b/src/locales/ja.json index f97d9562..580fbfb9 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -200,7 +200,7 @@ "versions.released": "リリース済み", "versions.resource_pack_format": "リソースパック形式", "versions.search": "バージョンを検索", - "versions.technical_changes": "技術的な変更点", + "versions.changelog": "技術的な変更点", "world": "ワールド設定 (World Settings)", "worldgen": "ワールドジェネレーター (World Generator)", "worldgen/biome": "バイオーム (Biome)", diff --git a/src/locales/ko.json b/src/locales/ko.json index a87ee36b..1889bb6b 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -186,7 +186,7 @@ "versions.released": "릴리즈됨", "versions.resource_pack_format": "리소스 팩 형식", "versions.search": "버전 검색", - "versions.technical_changes": "기술적 변경", + "versions.changelog": "기술적 변경", "world": "월드 설정", "worldgen": "월드젠", "worldgen/biome": "바이옴", diff --git a/src/locales/ru.json b/src/locales/ru.json index 0a2d65f1..407a5204 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -214,7 +214,7 @@ "versions.pack_format": "Формат набора", "versions.data_pack_format": "Формат набора данных", "versions.resource_pack_format": "Формат набора ресурсов", - "versions.technical_changes": "Технические изменения", + "versions.changelog": "Технические изменения", "versions.discussion": "Обсуждение", "versions.fixes": "Исправленные ошибки", "versions.fixes.no_results": "Нет исправлений", diff --git a/src/locales/zh-cn.json b/src/locales/zh-cn.json index 95c31de0..34d1080b 100644 --- a/src/locales/zh-cn.json +++ b/src/locales/zh-cn.json @@ -200,7 +200,7 @@ "versions.released": "发布于", "versions.resource_pack_format": "资源包格式", "versions.search": "搜索版本", - "versions.technical_changes": "技术变更", + "versions.changelog": "技术变更", "world": "世界设置", "worldgen": "世界生成", "worldgen/biome": "生物群系", diff --git a/src/styles/global.css b/src/styles/global.css index 32570ff3..4f2baa7f 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -38,6 +38,16 @@ --editor-string: #CE9178; --editor-constant: #569CD6; --editor-number: #B5CEA8; + --diff-added: #3fb950; + --diff-modified: #d29922; + --diff-removed: #f85149; + --diff-renamed: #7d8590; + --diff-line-added: rgba(46, 160, 67, 0.15); + --diff-line-removed: rgba(248, 81, 73, 0.1); + --diff-line-separation: rgba(56, 139, 253, 0.1); + --diff-numbers-added: rgba(63, 185, 80, 0.3); + --diff-numbers-removed: rgba(248, 81, 73, 0.3); + --diff-numbers-separation: rgba(56, 139, 253, 0.4); } :root[data-theme=light] { @@ -80,6 +90,16 @@ --editor-string: #A31515; --editor-constant: #0000FF; --editor-number: #098658; + --diff-added: #1a7f37; + --diff-modified: #9a6700; + --diff-removed: #d1242f; + --diff-renamed: #656d76; + --diff-line-added: rgb(230, 255, 236); + --diff-line-removed: rgb(255, 235, 233); + --diff-line-separation: rgb(221, 244, 255); + --diff-numbers-added: rgb(204, 255, 216); + --diff-numbers-removed: rgb(255, 215, 213); + --diff-numbers-separation: rgba(84, 174, 255, 0.4); } @media (prefers-color-scheme: light) { @@ -123,6 +143,16 @@ --editor-string: #A31515; --editor-constant: #0000FF; --editor-number: #098658; + --diff-added: #1a7f37; + --diff-modified: #9a6700; + --diff-removed: #d1242f; + --diff-renamed: #656d76; + --diff-line-added: rgb(230, 255, 236); + --diff-line-removed: rgb(255, 235, 233); + --diff-line-separation: rgb(221, 244, 255); + --diff-numbers-added: rgb(204, 255, 216); + --diff-numbers-removed: rgb(255, 215, 213); + --diff-numbers-separation: rgba(84, 174, 255, 0.4); } } @@ -2350,6 +2380,123 @@ hr { margin-top: 20px; } +.diff-header { + background-color: var(--background-1); +} + +.diff-toggle { + fill: var(--text-3); +} + +.diff-toggle:hover { + fill: var(--text-1); +} + +@media (min-width: 768px) { + .diff-tree { + max-height: calc(100vh - 56px); + min-height: calc(100vh - 56px); + } + + .diff-view-panel { + margin-top: calc(-100vh + 56px); + min-height: calc(100vh - 56px); + } +} + +.diff-entry { + position: relative; + display: flex; + align-items: center; + cursor: pointer; + padding: 4px 8px 4px 6px; + padding-left: calc(var(--indent, 0) * 8px); + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + color: var(--text-2); + border-left: 2px solid transparent; +} + +.diff-entry:hover { + background-color: var(--background-2); +} + +.diff-entry.active { + background-color: var(--background-4); + border-color: var(--accent-primary); +} + +.diff-added { + fill: var(--diff-added); +} + +.diff-modified { + fill: var(--diff-modified); +} + +.diff-removed { + fill: var(--diff-removed); +} + +.diff-renamed { + fill: var(--diff-renamed); +} + +.diff-view { + background-color: var(--background-1); + border: 1px solid var(--background-4); + border-left: none; + border-right: none; +} + +.diff-line-added { + background-color: var(--diff-line-added); + color: var(--text-1); +} + +.diff-line-removed { + background-color: var(--diff-line-removed); + color: var(--text-1); +} + +.diff-line-separation { + background-color: var(--diff-line-separation); +} + +.diff-number { + text-align: right; +} + +.diff-line-added .diff-number { + background-color: var(--diff-numbers-added); + color: var(--text-1); +} + +.diff-line-removed .diff-number { + background-color: var(--diff-numbers-removed); + color: var(--text-2); +} + +.diff-line-separation .diff-number { + background-color: var(--diff-numbers-separation); + text-align: center; +} + +.diff-media { + image-rendering: pixelated; + background-color: var(--background-2); +} + +.diff-media-added { + background-color: var(--diff-line-added); +} + +.diff-media-removed { + background-color: var(--diff-line-removed); +} + .tabs { display: flex; margin-bottom: 10px;