Monorepo for Tangled tangled.org
5

Configure Feed

Select the types of activity you want to include in your feed.

appview/repo: copy file contents to clipboard

this pull request adds a "copy to clipboard" button on both the readme
of a repository and the file view shown when viewing a specific file

Signed-off-by: eti <eti@eti.tf>

author
eti
committer
Tangled
date (Jun 1, 2026, 6:44 PM +0300) commit 512e495e parent f73a82be
+63 -2
+4
appview/models/repo.go
··· 236 236 func (b BlobView) ShowingText() bool { 237 237 return !b.ShowingRendered 238 238 } 239 + 240 + func (b BlobView) ShowCopy() bool { 241 + return b.ContentType.IsCode() || b.ContentType.IsMarkup() || b.ContentType.IsSvg() || b.ContentType.IsImage() 242 + }
+7 -1
appview/pages/templates/repo/blob.html
··· 48 48 <a href="/{{ .RepoInfo.FullName }}/raw/{{ pathEscape .Ref }}/{{ .Path }}">View raw</a> 49 49 {{ end }} 50 50 51 + {{ if .BlobView.ShowCopy }} 52 + <span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span> 53 + {{ template "repo/fragments/copyFileButton" (dict "Url" (printf "/%s/raw/%s/%s" .RepoInfo.FullName (pathEscape .Ref) .Path)) }} 54 + {{ end }} 55 + 51 56 {{ if .BlobView.ShowToggle }} 52 57 <span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span> 53 58 <a href="/{{ .RepoInfo.FullName }}/blob/{{ pathEscape .Ref }}/{{ .Path }}?code={{ .BlobView.ShowingRendered }}" hx-boost="true"> ··· 58 63 {{ if .BlobView.ShowingText }} 59 64 <div id="toggle-wrap-content" class="flex items-center"> 60 65 <span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span> 61 - <label class="flex lowercase font-normal px-1 py-0 gap-1 text-xs md:text-sm"> 66 + <label class="flex items-center lowercase font-normal px-1 py-0 gap-1 text-xs md:text-sm"> 62 67 <input id="toggle-wrap-content-checkbox" type="checkbox" name="wrap"/> 63 68 wrap content 64 69 </label> ··· 125 130 </div> 126 131 {{ end }} 127 132 {{ template "fragments/multiline-select" }} 133 + {{ template "repo/fragments/copyFileScript" }} 128 134 <script> 129 135 (() => { 130 136 const abortController = new AbortController();
+9
appview/pages/templates/repo/fragments/copyFileButton.html
··· 1 + {{ define "repo/fragments/copyFileButton" }} 2 + <button type="button" 3 + class="btn-flat gap-1.5" 4 + onclick="copyRawFile(this, '{{ .Url }}')"> 5 + <span class="copy-check hidden">{{ i "check" "size-3.5" }}</span> 6 + <span class="copy-label">copy to clipboard</span> 7 + <span class="copy-spinner hidden">{{ i "loader-circle" "size-3.5 animate-spin" }}</span> 8 + </button> 9 + {{ end }}
+38
appview/pages/templates/repo/fragments/copyFileScript.html
··· 1 + {{ define "repo/fragments/copyFileScript" }} 2 + <script> 3 + async function copyRawFile(btn, url) { 4 + const label = btn.querySelector('.copy-label'); 5 + const spinner = btn.querySelector('.copy-spinner'); 6 + label.textContent = 'copying'; 7 + spinner.classList.remove('hidden'); 8 + btn.disabled = true; 9 + try { 10 + const res = await fetch(url); 11 + if (!res.ok) throw new Error(); 12 + const contentType = res.headers.get('Content-Type') || ''; 13 + if (contentType.startsWith('image/')) { 14 + const blob = await res.blob(); 15 + await navigator.clipboard.write([ 16 + new ClipboardItem({ [contentType]: blob }) 17 + ]); 18 + } else { 19 + const text = await res.text(); 20 + await navigator.clipboard.writeText(text); 21 + } 22 + spinner.classList.add('hidden'); 23 + label.textContent = 'copied'; 24 + btn.querySelector('.copy-check').classList.remove('hidden'); 25 + btn.disabled = false; 26 + setTimeout(() => { 27 + label.textContent = 'copy to clipboard'; 28 + btn.querySelector('.copy-check').classList.add('hidden'); 29 + }, 2500); 30 + } catch { 31 + spinner.classList.add('hidden'); 32 + label.textContent = 'error'; 33 + btn.disabled = false; 34 + setTimeout(() => { label.textContent = 'copy to clipboard'; }, 2500); 35 + } 36 + } 37 + </script> 38 + {{ end }}
+5 -1
appview/pages/templates/repo/fragments/readme.html
··· 1 1 {{ define "repo/fragments/readme" }} 2 2 <div class="mt-4 rounded bg-white dark:bg-gray-800 drop-shadow-sm w-full mx-auto overflow-hidden"> 3 3 {{- if .ReadmeFileName -}} 4 - <div class="px-4 py-2 border-b border-gray-200 dark:border-gray-600 flex items-center gap-2"> 4 + <div class="px-4 py-2 border-b border-gray-200 dark:border-gray-600 flex justify-between"> 5 + <span class="flex items-center gap-2"> 5 6 {{ i "file-text" "w-4 h-4" "text-gray-600 dark:text-gray-400" }} 6 7 <span class="font-mono text-sm text-gray-800 dark:text-gray-200">{{ .ReadmeFileName }}</span> 8 + </span> 9 + {{ template "repo/fragments/copyFileButton" (dict "Url" (printf "/%s/raw/%s/%s" .RepoInfo.FullName (pathEscape .Ref) .ReadmeFileName)) }} 7 10 </div> 8 11 {{- end -}} 9 12 <section ··· 21 24 {{- end -}}</article> 22 25 </section> 23 26 </div> 27 + {{ template "repo/fragments/copyFileScript" }} 24 28 {{ end }}