{#if state.error.includes('not cloned locally') && hasUnlimitedAccess($userStore.userLevel)}
{/if}
{/if}
{#if repoOwnerPubkeyDerived}
{#if state.ui.activeTab === 'files'}
{
state.files.currentPath = path;
loadFiles(path);
}}
onNavigateBack={handleBack}
onContentChange={(content) => {
state.files.editedContent = content;
state.files.hasChanges = content !== state.files.content;
}}
isMaintainer={state.maintainers.isMaintainer}
showFilePreview={state.preview.file.showPreview}
fileHtml={state.preview.file.html}
highlightedFileContent={state.preview.file.highlightedContent}
isImageFile={state.preview.file.isImage}
imageUrl={state.preview.file.imageUrl}
wordWrap={state.ui.wordWrap}
{supportsPreview}
onSave={() => {
if (!state.user.pubkey || !state.maintainers.isMaintainer || needsClone) return;
state.openDialog = 'commit';
}}
onTogglePreview={() => {
state.preview.file.showPreview = !state.preview.file.showPreview;
// When switching to raw mode, ensure syntax highlighting is applied
if (!state.preview.file.showPreview && state.files.content && state.files.currentFile) {
const ext = state.files.currentFile.split('.').pop() || '';
// Only apply if we don't already have highlighted content
if (!state.preview.file.highlightedContent || state.preview.file.highlightedContent.trim() === '') {
applySyntaxHighlighting(state.files.content, ext).catch(err => console.error('Error applying syntax highlighting:', err));
}
}
}}
onCopyFileContent={copyFileContent}
onDownloadFile={downloadFile}
copyingFile={state.preview.copying}
saving={state.saving}
needsClone={needsClone}
{cloneTooltip}
branches={state.git.branches}
currentBranch={state.git.currentBranch}
defaultBranch={state.git.defaultBranch}
onBranchChange={(branch) => {
state.git.currentBranch = branch;
handleBranchChangeDirect(branch);
}}
userPubkey={state.user.pubkey}
activeTab={state.ui.activeTab}
{tabs}
onTabChange={(tab: string) => {
state.ui.activeTab = tab as typeof state.ui.activeTab;
// Update URL query parameter without page reload
const url = new URL($page.url);
if (tab === 'docs') {
url.searchParams.delete('tab');
} else {
url.searchParams.set('tab', tab);
}
goto(url.pathname + url.search, { replaceState: true, noScroll: true });
}}
onCreateFile={() => {
if (!state.user.pubkey || !state.maintainers.isMaintainer || needsClone) return;
state.openDialog = 'createFile';
}}
onApplySyntaxHighlighting={applySyntaxHighlighting}
onRenderPreview={async (content: string, ext: string) => {
return new Promise((resolve) => {
const branch = state.git.currentBranch || state.git.defaultBranch || null;
renderFileAsHtmlUtil(content, ext, state.files.currentFile, (html: string) => {
resolve(html);
}, state.npub, state.repo, branch);
});
}}
npub={state.npub}
repo={state.repo}
/>
{/if}
{#if state.ui.activeTab === 'history'}
{
state.git.selectedCommit = hash;
viewDiff(hash);
}}
onVerify={async (hash) => {
state.git.verifyingCommits.add(hash);
try {
// Trigger verification logic - find the commit and verify
const commit = state.git.commits.find((c: { hash?: string; sha?: string }) => (c.hash || c.sha) === hash);
if (commit) {
await verifyCommit(hash);
}
} finally {
state.git.verifyingCommits.delete(hash);
}
}}
verifyingCommits={state.git.verifyingCommits}
showDiff={state.git.showDiff}
diffData={state.git.diffData}
activeTab={state.ui.activeTab}
{tabs}
onTabChange={(tab: string) => {
state.ui.activeTab = tab as typeof state.ui.activeTab;
// Update URL query parameter without page reload
const url = new URL($page.url);
if (tab === 'docs') {
url.searchParams.delete('tab');
} else {
url.searchParams.set('tab', tab);
}
goto(url.pathname + url.search, { replaceState: true, noScroll: true });
}}
/>
{/if}
state.git.selectedTag = tagName}
onTabChange={(tab: string) => {
state.ui.activeTab = tab as typeof state.ui.activeTab;
// Update URL without page reload
const url = new URL($page.url);
if (tab === 'docs') {
url.searchParams.delete('tab');
} else {
url.searchParams.set('tab', tab);
}
goto(url.pathname + url.search, { replaceState: true, noScroll: true });
}}
onToggleMobilePanel={() => state.ui.showLeftPanelOnMobile = !state.ui.showLeftPanelOnMobile}
onCreateTag={() => state.openDialog = 'createTag'}
onCreateRelease={(tagName, tagHash) => {
state.forms.release.tagName = tagName;
state.forms.release.tagHash = tagHash;
// Pre-fill download URL with full URL
if (typeof window !== 'undefined') {
const origin = window.location.origin;
state.forms.release.downloadUrl = `${origin}/api/repos/${state.npub}/${state.repo}/archive?ref=${encodeURIComponent(tagName)}&format=zip`;
} else {
state.forms.release.downloadUrl = `/api/repos/${state.npub}/${state.repo}/archive?ref=${encodeURIComponent(tagName)}&format=zip`;
}
state.openDialog = 'createRelease';
}}
onLoadTags={loadTags}
/>
{#if state.ui.activeTab === 'code-search'}
{/if}
{#if state.ui.activeTab === 'issues'}
{
state.selected.issue = id;
loadIssueReplies(id);
}}
onStatusUpdate={async (id, status) => {
// Find issue and update status
const issue = state.issues.find((i: { id: string }) => i.id === id);
if (issue) {
await updateIssueStatus(id, issue.author, status as 'open' | 'closed' | 'resolved' | 'draft');
await loadIssues();
}
}}
issueReplies={state.issueReplies}
loadingReplies={state.loading.issueReplies}
activeTab={state.ui.activeTab}
{tabs}
onTabChange={(tab: string) => {
state.ui.activeTab = tab as typeof state.ui.activeTab;
// Update URL query parameter without page reload
const url = new URL($page.url);
if (tab === 'docs') {
url.searchParams.delete('tab');
} else {
url.searchParams.set('tab', tab);
}
goto(url.pathname + url.search, { replaceState: true, noScroll: true });
}}
onCreate={() => { state.openDialog = 'createIssue'; }}
userPubkey={state.user.pubkey || null}
/>
{/if}
{#if state.ui.activeTab === 'prs'}
{
state.selected.pr = id;
}}
onStatusUpdate={async (id, status) => {
// Find PR and update status - similar to updateIssueStatus
const pr = state.prs.find((p: { id: string }) => p.id === id);
if (pr && state.user.pubkeyHex) {
// Check if user is maintainer or PR author
const isAuthor = state.user.pubkeyHex === pr.author;
if (!state.maintainers.isMaintainer && !isAuthor) {
alert('Only repository maintainers or PR authors can update PR status');
return;
}
try {
const response = await fetch(`/api/repos/${state.npub}/${state.repo}/pull-requests/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prAuthor: pr.author,
status
})
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.state.error || 'Failed to update PR status');
}
await loadPRs();
} catch (err) {
state.error = err instanceof Error ? err.message : 'Failed to update PR status';
console.error('Error updating PR status:', err);
}
}
}}
activeTab={state.ui.activeTab}
{tabs}
onTabChange={(tab: string) => {
state.ui.activeTab = tab as typeof state.ui.activeTab;
// Update URL query parameter without page reload
const url = new URL($page.url);
if (tab === 'docs') {
url.searchParams.delete('tab');
} else {
url.searchParams.set('tab', tab);
}
goto(url.pathname + url.search, { replaceState: true, noScroll: true });
}}
onCreate={() => { state.openDialog = 'createPR'; }}
userPubkey={state.user.pubkey || null}
/>
{/if}
{#if state.ui.activeTab === 'patches'}
{
state.selected.patch = id;
}}
onStatusUpdate={async (id, status) => {
const patch = state.patches.find((p: { id: string }) => p.id === id);
if (patch) {
await updatePatchStatus(id, patch.author, status);
}
}}
onApply={async (id) => {
applying[id] = true;
try {
const patch = state.patches.find((p: { id: string }) => p.id === id);
if (!patch) {
throw new Error('Patch not found');
}
if (!state.user.pubkey || !state.maintainers.isMaintainer || needsClone) {
alert('Only maintainers can apply patches');
return;
}
// Check if repo is empty (no branches)
if (state.git.branches.length === 0) {
alert('Cannot apply patch to an empty repository. Please create a branch first.');
return;
}
if (!confirm('Apply this patch to the repository? This will create a commit with the patch changes.')) {
return;
}
const authorEmail = await getUserEmail();
const authorName = await getUserName();
// Use current branch, default branch, or first branch (repo is not empty at this point)
const branch = state.git.currentBranch || state.git.defaultBranch ||
(state.git.branches.length > 0
? (typeof state.git.branches[0] === 'string' ? state.git.branches[0] : state.git.branches[0].name)
: null);
if (!branch) {
alert('No branch available to apply patch to. Please create a branch first.');
return;
}
const response = await fetch(`/api/repos/${state.npub}/${state.repo}/patches/${id}/apply`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...buildApiHeaders()
},
body: JSON.stringify({
message: `Apply patch ${id.slice(0, 8)}: ${patch.subject}`,
authorName,
authorEmail,
branch
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Failed to apply patch');
}
await loadPatches();
alert('Patch applied successfully!');
} catch (err) {
state.error = err instanceof Error ? err.message : 'Failed to apply patch';
console.error('Error applying patch:', err);
} finally {
applying[id] = false;
}
}}
{applying}
activeTab={state.ui.activeTab}
{tabs}
onTabChange={(tab: string) => {
state.ui.activeTab = tab as typeof state.ui.activeTab;
// Update URL query parameter without page reload
const url = new URL($page.url);
if (tab === 'docs') {
url.searchParams.delete('tab');
} else {
url.searchParams.set('tab', tab);
}
goto(url.pathname + url.search, { replaceState: true, noScroll: true });
}}
onCreate={() => { state.openDialog = 'createPatch'; }}
userPubkey={state.user.pubkey || null}
/>
{/if}
{#if state.ui.activeTab === 'discussions'}
{
state.ui.activeTab = tab as typeof state.ui.activeTab;
// Update URL query parameter without page reload
const url = new URL($page.url);
if (tab === 'docs') {
url.searchParams.delete('tab');
} else {
url.searchParams.set('tab', tab);
}
goto(url.pathname + url.search, { replaceState: true, noScroll: true });
}}
/>
{/if}
{#if state.ui.activeTab === 'docs' || (!['files', 'issues', 'prs', 'patches', 'discussions', 'history', 'tags', 'code-search'].includes(state.ui.activeTab))}
{
state.ui.activeTab = tab as typeof state.ui.activeTab;
// Update URL query parameter without page reload
const url = new URL($page.url);
if (tab === 'docs') {
url.searchParams.delete('tab');
} else {
url.searchParams.set('tab', tab);
}
goto(url.pathname + url.search, { replaceState: true, noScroll: true });
}}
isMaintainer={state.maintainers.isMaintainer}
reloadTrigger={docsReloadTrigger}
onCreateDocumentation={() => {
if (!state.user.pubkey || !state.maintainers.isMaintainer) return;
state.openDialog = 'createDocumentation';
}}
/>
{/if}
{#if state.ui.activeTab === 'code-search'}
{#if state.files.list.length === 0 && !state.loading.main}
{:else}
{#if state.loading.codeSearch}
{:else if state.codeSearch.results.length > 0}
{/each}
{:else if state.codeSearch.query.trim() && !state.loading.codeSearch}
{/if}
{/if}
This repo is empty and contains no files.
e.key === 'Enter' && performCodeSearch()}
class="code-search-input"
/>
Searching...
Found {state.codeSearch.results.length} result{state.codeSearch.results.length !== 1 ? 's' : ''}
{#each state.codeSearch.results as result}
{result.file}
Line {result.line}
{#if state.codeSearch.scope === 'all' && 'repo' in result}
{result.repo || state.npub}/{result.repo || state.repo}
{/if}
{result.content}
{#if state.error}
{:else}
{/if}
{/if}
No results found
{/if}