From 5b971495fffd73c73be9a72231ae5f2fa3488dc3 Mon Sep 17 00:00:00 2001 From: Eduardo Oliveira Date: Sun, 8 Sep 2024 18:55:11 +0100 Subject: [PATCH] A lot of things, plus initial search --- frontend/public/index.css | 688 +++++++++++++++++++-- frontend/src/lib/main.js | 12 +- v2/library/entities/asset.go | 34 +- v2/library/process/process.go | 6 +- v2/library/repo/asset.go | 12 + v2/library/web/comp/assetcard.templ | 10 +- v2/library/web/comp/details.templ | 41 +- v2/library/web/comp/edit.templ | 37 +- v2/library/web/comp/expand_asset_btn.templ | 20 - v2/library/web/comp/index.templ | 32 +- v2/library/web/comp/kind_filter.templ | 26 - v2/library/web/comp/models.go | 19 +- v2/library/web/comp/search.templ | 50 ++ v2/library/web/comp/searchresult.templ | 5 + v2/library/web/comp/sidebar.templ | 1 + v2/library/web/comp/view3d.templ | 47 +- v2/library/web/details.go | 10 +- v2/library/web/edit.go | 122 ++-- v2/library/web/index.go | 147 +++-- v2/library/web/list.go | 26 +- v2/library/web/new.go | 30 +- v2/library/web/sidebar.go | 16 - v2/library/web/viewer.go | 38 ++ v2/library/web/web.go | 10 +- v2/web/index.go | 2 +- v2/web/web.go | 10 + v2/web/writter.go | 23 +- 27 files changed, 1158 insertions(+), 316 deletions(-) delete mode 100644 v2/library/web/comp/expand_asset_btn.templ delete mode 100644 v2/library/web/comp/kind_filter.templ create mode 100644 v2/library/web/comp/search.templ create mode 100644 v2/library/web/comp/searchresult.templ delete mode 100644 v2/library/web/sidebar.go create mode 100644 v2/library/web/viewer.go diff --git a/frontend/public/index.css b/frontend/public/index.css index 003d212..aa19169 100644 --- a/frontend/public/index.css +++ b/frontend/public/index.css @@ -788,6 +788,30 @@ html { } } +.avatar { + position: relative; + display: inline-flex; +} + +.avatar > div { + display: block; + aspect-ratio: 1 / 1; + overflow: hidden; +} + +.avatar img { + height: 100%; + width: 100%; + -o-object-fit: cover; + object-fit: cover; +} + +.avatar.placeholder > div { + display: flex; + align-items: center; + justify-content: center; +} + .breadcrumbs { max-width: 100%; overflow-x: auto; @@ -838,9 +862,10 @@ html { --tw-text-opacity: 1; } - .tabs-boxed :is(.tab-active, [aria-selected="true"]):not(.tab-disabled):not([disabled]):hover, .tabs-boxed :is(input:checked):hover { - --tw-text-opacity: 1; - color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity))); + .table tr.hover:hover, + .table tr.hover:nth-child(even):hover { + --tw-bg-opacity: 1; + background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); } } @@ -894,6 +919,13 @@ html { padding: 0px; } +.btn-circle { + height: 3rem; + width: 3rem; + border-radius: 9999px; + padding: 0px; +} + :where(.btn:is(input[type="checkbox"])), :where(.btn:is(input[type="radio"])) { width: auto; @@ -908,6 +940,85 @@ html { content: var(--tw-content); } +.card { + position: relative; + display: flex; + flex-direction: column; + border-radius: var(--rounded-box, 1rem); +} + +.card:focus { + outline: 2px solid transparent; + outline-offset: 2px; +} + +.card-body { + display: flex; + flex: 1 1 auto; + flex-direction: column; + padding: var(--padding-card, 2rem); + gap: 0.5rem; +} + +.card-body :where(p) { + flex-grow: 1; +} + +.card figure { + display: flex; + align-items: center; + justify-content: center; +} + +.card.image-full { + display: grid; +} + +.card.image-full:before { + position: relative; + content: ""; + z-index: 10; + border-radius: var(--rounded-box, 1rem); + --tw-bg-opacity: 1; + background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); + opacity: 0.75; +} + +.card.image-full:before, + .card.image-full > * { + grid-column-start: 1; + grid-row-start: 1; +} + +.card.image-full > figure img { + height: 100%; + -o-object-fit: cover; + object-fit: cover; +} + +.card.image-full > .card-body { + position: relative; + z-index: 20; + --tw-text-opacity: 1; + color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity))); +} + +.checkbox { + flex-shrink: 0; + --chkbg: var(--fallback-bc,oklch(var(--bc)/1)); + --chkfg: var(--fallback-b1,oklch(var(--b1)/1)); + height: 1.5rem; + width: 1.5rem; + cursor: pointer; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border-radius: var(--rounded-btn, 0.5rem); + border-width: 1px; + border-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity))); + --tw-border-opacity: 0.2; +} + .dropdown { position: relative; display: inline-block; @@ -1132,6 +1243,38 @@ html { padding-bottom: 0.5rem; } +.hero { + display: grid; + width: 100%; + place-items: center; + background-size: cover; + background-position: center; +} + +.hero > * { + grid-column-start: 1; + grid-row-start: 1; +} + +.hero-overlay { + grid-column-start: 1; + grid-row-start: 1; + height: 100%; + width: 100%; + background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); + --tw-bg-opacity: 0.5; +} + +.hero-content { + z-index: 0; + display: flex; + align-items: center; + justify-content: center; + max-width: 80rem; + gap: 1rem; + padding: 1rem; +} + .input { flex-shrink: 1; -webkit-appearance: none; @@ -1342,11 +1485,6 @@ html { height: auto; } -.tabs { - display: grid; - align-items: flex-end; -} - .tabs-lifted:has(.tab-content[class^="rounded-"]) .tab:first-child:not(:is(.tab-active, [aria-selected="true"])), .tabs-lifted:has(.tab-content[class*=" rounded-"]) .tab:first-child:not(:is(.tab-active, [aria-selected="true"])) { @@ -1398,16 +1536,44 @@ html { grid-column-start: span 9999; } -:checked + .tab-content:nth-child(2), - :is(.tab-active, [aria-selected="true"]) + .tab-content:nth-child(2) { - border-start-start-radius: 0px; -} - input.tab:checked + .tab-content, :is(.tab-active, [aria-selected="true"]) + .tab-content { display: block; } +.table { + position: relative; + width: 100%; + border-radius: var(--rounded-box, 1rem); + text-align: left; + font-size: 0.875rem; + line-height: 1.25rem; +} + +.table :where(.table-pin-rows thead tr) { + position: sticky; + top: 0px; + z-index: 1; + --tw-bg-opacity: 1; + background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); +} + +.table :where(.table-pin-rows tfoot tr) { + position: sticky; + bottom: 0px; + z-index: 1; + --tw-bg-opacity: 1; + background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); +} + +.table :where(.table-pin-cols tr th) { + position: sticky; + left: 0px; + right: 0px; + --tw-bg-opacity: 1; + background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); +} + .textarea { min-height: 3rem; flex-shrink: 1; @@ -1425,6 +1591,38 @@ input.tab:checked + .tab-content, background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); } +.toggle { + flex-shrink: 0; + --tglbg: var(--fallback-b1,oklch(var(--b1)/1)); + --handleoffset: 1.5rem; + --handleoffsetcalculator: calc(var(--handleoffset) * -1); + --togglehandleborder: 0 0; + height: 1.5rem; + width: 3rem; + cursor: pointer; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border-radius: var(--rounded-badge, 1.9rem); + border-width: 1px; + border-color: currentColor; + background-color: currentColor; + color: var(--fallback-bc,oklch(var(--bc)/0.5)); + transition: background, + box-shadow var(--animation-input, 0.2s) ease-out; + box-shadow: var(--handleoffsetcalculator) 0 0 2px var(--tglbg) inset, + 0 0 0 2px var(--tglbg) inset, + var(--togglehandleborder); +} + +.avatar-group :where(.avatar) { + overflow: hidden; + border-radius: 9999px; + border-width: 4px; + --tw-border-opacity: 1; + border-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-border-opacity))); +} + .btm-nav > *:where(.active) { border-top-width: 2px; --tw-bg-opacity: 1; @@ -1567,6 +1765,92 @@ input.tab:checked + .tab-content, } } +.card :where(figure:first-child) { + overflow: hidden; + border-start-start-radius: inherit; + border-start-end-radius: inherit; + border-end-start-radius: unset; + border-end-end-radius: unset; +} + +.card :where(figure:last-child) { + overflow: hidden; + border-start-start-radius: unset; + border-start-end-radius: unset; + border-end-start-radius: inherit; + border-end-end-radius: inherit; +} + +.card:focus-visible { + outline: 2px solid currentColor; + outline-offset: 2px; +} + +.card.bordered { + border-width: 1px; + --tw-border-opacity: 1; + border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity))); +} + +.card.compact .card-body { + padding: 1rem; + font-size: 0.875rem; + line-height: 1.25rem; +} + +.card.image-full :where(figure) { + overflow: hidden; + border-radius: inherit; +} + +.checkbox:focus { + box-shadow: none; +} + +.checkbox:focus-visible { + outline-style: solid; + outline-width: 2px; + outline-offset: 2px; + outline-color: var(--fallback-bc,oklch(var(--bc)/1)); +} + +.checkbox:disabled { + border-width: 0px; + cursor: not-allowed; + border-color: transparent; + --tw-bg-opacity: 1; + background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity))); + opacity: 0.2; +} + +.checkbox:checked, + .checkbox[aria-checked="true"] { + background-repeat: no-repeat; + animation: checkmark var(--animation-input, 0.2s) ease-out; + background-color: var(--chkbg); + background-image: linear-gradient(-45deg, transparent 65%, var(--chkbg) 65.99%), + linear-gradient(45deg, transparent 75%, var(--chkbg) 75.99%), + linear-gradient(-45deg, var(--chkbg) 40%, transparent 40.99%), + linear-gradient( + 45deg, + var(--chkbg) 30%, + var(--chkfg) 30.99%, + var(--chkfg) 40%, + transparent 40.99% + ), + linear-gradient(-45deg, var(--chkfg) 50%, var(--chkbg) 50.99%); +} + +.checkbox:indeterminate { + --tw-bg-opacity: 1; + background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity))); + background-repeat: no-repeat; + animation: checkmark var(--animation-input, 0.2s) ease-out; + background-image: linear-gradient(90deg, transparent 80%, var(--chkbg) 80%), + linear-gradient(-90deg, transparent 80%, var(--chkbg) 80%), + linear-gradient(0deg, var(--chkbg) 43%, var(--chkfg) 43%, var(--chkfg) 57%, var(--chkbg) 57%); +} + @keyframes checkmark { 0% { background-position-y: 5px; @@ -1589,6 +1873,13 @@ input.tab:checked + .tab-content, transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } +.label-text { + font-size: 0.875rem; + line-height: 1.25rem; + --tw-text-opacity: 1; + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); +} + .input input { --tw-bg-opacity: 1; background-color: var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity))); @@ -1604,6 +1895,10 @@ input.tab:checked + .tab-content, line-height: 1em; } +.input-bordered { + border-color: var(--fallback-bc,oklch(var(--bc)/0.2)); +} + .input:focus, .input:focus-within { box-shadow: none; @@ -2071,22 +2366,20 @@ input.tab:checked + .tab-content, background-position: top right; } -.tabs-boxed { +.tabs-boxed .tab { border-radius: var(--rounded-btn, 0.5rem); - --tw-bg-opacity: 1; - background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); - padding: 0.25rem; } -.tabs-boxed .tab { - border-radius: var(--rounded-btn, 0.5rem); +.table:where([dir="rtl"], [dir="rtl"] *) { + text-align: right; } -.tabs-boxed :is(.tab-active, [aria-selected="true"]):not(.tab-disabled):not([disabled]), .tabs-boxed :is(input:checked) { - --tw-bg-opacity: 1; - background-color: var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity))); - --tw-text-opacity: 1; - color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity))); +.table :where(th, td) { + padding-left: 1rem; + padding-right: 1rem; + padding-top: 0.75rem; + padding-bottom: 0.75rem; + vertical-align: middle; } .table tr.active, @@ -2103,6 +2396,26 @@ input.tab:checked + .tab-content, background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity))); } +.table :where(thead tr, tbody tr:not(:last-child), tbody tr:first-child:last-child) { + border-bottom-width: 1px; + --tw-border-opacity: 1; + border-bottom-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity))); +} + +.table :where(thead, tfoot) { + white-space: nowrap; + font-size: 0.75rem; + line-height: 1rem; + font-weight: 700; + color: var(--fallback-bc,oklch(var(--bc)/0.6)); +} + +.table :where(tfoot) { + border-top-width: 1px; + --tw-border-opacity: 1; + border-top-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity))); +} + .textarea:focus { box-shadow: none; border-color: var(--fallback-bc,oklch(var(--bc)/0.2)); @@ -2147,6 +2460,71 @@ input.tab:checked + .tab-content, } } +[dir="rtl"] .toggle { + --handleoffsetcalculator: calc(var(--handleoffset) * 1); +} + +.toggle:focus-visible { + outline-style: solid; + outline-width: 2px; + outline-offset: 2px; + outline-color: var(--fallback-bc,oklch(var(--bc)/0.2)); +} + +.toggle:hover { + background-color: currentColor; +} + +.toggle:checked, + .toggle[aria-checked="true"] { + background-image: none; + --handleoffsetcalculator: var(--handleoffset); + --tw-text-opacity: 1; + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); +} + +[dir="rtl"] .toggle:checked, [dir="rtl"] .toggle[aria-checked="true"] { + --handleoffsetcalculator: calc(var(--handleoffset) * -1); +} + +.toggle:indeterminate { + --tw-text-opacity: 1; + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); + box-shadow: calc(var(--handleoffset) / 2) 0 0 2px var(--tglbg) inset, + calc(var(--handleoffset) / -2) 0 0 2px var(--tglbg) inset, + 0 0 0 2px var(--tglbg) inset; +} + +[dir="rtl"] .toggle:indeterminate { + box-shadow: calc(var(--handleoffset) / 2) 0 0 2px var(--tglbg) inset, + calc(var(--handleoffset) / -2) 0 0 2px var(--tglbg) inset, + 0 0 0 2px var(--tglbg) inset; +} + +.toggle-primary:focus-visible { + outline-color: var(--fallback-p,oklch(var(--p)/1)); +} + +.toggle-primary:checked, + .toggle-primary[aria-checked="true"] { + border-color: var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity))); + --tw-border-opacity: 0.1; + --tw-bg-opacity: 1; + background-color: var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity))); + --tw-text-opacity: 1; + color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity))); +} + +.toggle:disabled { + cursor: not-allowed; + --tw-border-opacity: 1; + border-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity))); + background-color: transparent; + opacity: 0.3; + --togglehandleborder: 0 0 0 3px var(--fallback-bc,oklch(var(--bc)/1)) inset, + var(--handleoffsetcalculator) 0 0 3px var(--fallback-bc,oklch(var(--bc)/1)) inset; +} + .btm-nav-xs > *:where(.active) { border-top-width: 1px; } @@ -2217,6 +2595,20 @@ input.tab:checked + .tab-content, padding: 0px; } +.btn-circle:where(.btn-md) { + height: 3rem; + width: 3rem; + border-radius: 9999px; + padding: 0px; +} + +.btn-circle:where(.btn-lg) { + height: 4rem; + width: 4rem; + border-radius: 9999px; + padding: 0px; +} + .join.join-vertical { flex-direction: column; } @@ -2325,6 +2717,56 @@ input.tab:checked + .tab-content, bottom: auto; } +.avatar.online:before { + content: ""; + position: absolute; + z-index: 10; + display: block; + border-radius: 9999px; + --tw-bg-opacity: 1; + background-color: var(--fallback-su,oklch(var(--su)/var(--tw-bg-opacity))); + outline-style: solid; + outline-width: 2px; + outline-color: var(--fallback-b1,oklch(var(--b1)/1)); + width: 15%; + height: 15%; + top: 7%; + right: 7%; +} + +.avatar.offline:before { + content: ""; + position: absolute; + z-index: 10; + display: block; + border-radius: 9999px; + --tw-bg-opacity: 1; + background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity))); + outline-style: solid; + outline-width: 2px; + outline-color: var(--fallback-b1,oklch(var(--b1)/1)); + width: 15%; + height: 15%; + top: 7%; + right: 7%; +} + +.card-compact .card-body { + padding: 1rem; + font-size: 0.875rem; + line-height: 1.25rem; +} + +.card-compact .card-title { + margin-bottom: 0.25rem; +} + +.card-normal .card-body { + padding: var(--padding-card, 2rem); + font-size: 1rem; + line-height: 1.5rem; +} + .join.join-vertical > :where(*:not(:first-child)) { margin-left: 0px; margin-right: 0px; @@ -2345,6 +2787,18 @@ input.tab:checked + .tab-content, margin-inline-start: calc(var(--border-btn) * -1); } +.table-xs :not(thead):not(tfoot) tr { + font-size: 0.75rem; + line-height: 1rem; +} + +.table-xs :where(th, td) { + padding-left: 0.5rem; + padding-right: 0.5rem; + padding-top: 0.25rem; + padding-bottom: 0.25rem; +} + .tooltip { position: relative; display: inline-block; @@ -2490,6 +2944,10 @@ input.tab:checked + .tab-content, z-index: 10; } +.z-20 { + z-index: 20; +} + .z-\[1\] { z-index: 1; } @@ -2508,10 +2966,26 @@ input.tab:checked + .tab-content, margin-bottom: 0.5rem; } +.mb-5 { + margin-bottom: 1.25rem; +} + +.mt-1 { + margin-top: 0.25rem; +} + .mt-2 { margin-top: 0.5rem; } +.mt-3 { + margin-top: 0.75rem; +} + +.mt-6 { + margin-top: 1.5rem; +} + .block { display: block; } @@ -2524,10 +2998,22 @@ input.tab:checked + .tab-content, display: flex; } +.inline-flex { + display: inline-flex; +} + +.table { + display: table; +} + .hidden { display: none; } +.h-12 { + height: 3rem; +} + .h-5 { height: 1.25rem; } @@ -2536,6 +3022,10 @@ input.tab:checked + .tab-content, height: 14rem; } +.h-6 { + height: 1.5rem; +} + .h-72 { height: 18rem; } @@ -2544,6 +3034,10 @@ input.tab:checked + .tab-content, height: 100vh; } +.w-12 { + width: 3rem; +} + .w-5 { width: 1.25rem; } @@ -2552,6 +3046,14 @@ input.tab:checked + .tab-content, width: 13rem; } +.w-6 { + width: 1.5rem; +} + +.w-64 { + width: 16rem; +} + .w-96 { width: 24rem; } @@ -2568,6 +3070,14 @@ input.tab:checked + .tab-content, max-width: 14rem; } +.max-w-80 { + max-width: 20rem; +} + +.max-w-xs { + max-width: 20rem; +} + .flex-1 { flex: 1 1 0%; } @@ -2592,8 +3102,8 @@ input.tab:checked + .tab-content, flex-basis: 14rem; } -.transform { - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +.cursor-pointer { + cursor: pointer; } .flex-row { @@ -2632,10 +3142,18 @@ input.tab:checked + .tab-content, justify-content: center; } +.justify-between { + justify-content: space-between; +} + .self-start { align-self: flex-start; } +.overflow-x-auto { + overflow-x: auto; +} + .overflow-y-auto { overflow-y: auto; } @@ -2671,6 +3189,10 @@ input.tab:checked + .tab-content, border-color: rgb(229 231 235 / var(--tw-border-opacity)); } +.border-transparent { + border-color: transparent; +} + .bg-base-100 { --tw-bg-opacity: 1; background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); @@ -2681,14 +3203,24 @@ input.tab:checked + .tab-content, background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); } +.bg-base-300 { + --tw-bg-opacity: 1; + background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity))); +} + +.bg-blue-600 { + --tw-bg-opacity: 1; + background-color: rgb(37 99 235 / var(--tw-bg-opacity)); +} + .bg-gray-300 { --tw-bg-opacity: 1; background-color: rgb(209 213 219 / var(--tw-bg-opacity)); } -.bg-gray-800 { +.bg-gray-600 { --tw-bg-opacity: 1; - background-color: rgb(31 41 55 / var(--tw-bg-opacity)); + background-color: rgb(75 85 99 / var(--tw-bg-opacity)); } .bg-primary { @@ -2706,6 +3238,10 @@ input.tab:checked + .tab-content, background-color: rgb(255 255 255 / var(--tw-bg-opacity)); } +.bg-opacity-60 { + --tw-bg-opacity: 0.6; +} + .bg-cover { background-size: cover; } @@ -2741,16 +3277,15 @@ input.tab:checked + .tab-content, padding-right: 1rem; } -.py-1 { - padding-top: 0.25rem; - padding-bottom: 0.25rem; -} - .py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; } +.pb-1 { + padding-bottom: 0.25rem; +} + .pb-2 { padding-bottom: 0.5rem; } @@ -2759,6 +3294,29 @@ input.tab:checked + .tab-content, padding-right: 0.25rem; } +.pt-1 { + padding-top: 0.25rem; +} + +.text-left { + text-align: left; +} + +.text-2xl { + font-size: 1.5rem; + line-height: 2rem; +} + +.text-5xl { + font-size: 3rem; + line-height: 1; +} + +.text-lg { + font-size: 1.125rem; + line-height: 1.75rem; +} + .text-sm { font-size: 0.875rem; line-height: 1.25rem; @@ -2769,17 +3327,16 @@ input.tab:checked + .tab-content, line-height: 1.75rem; } -.text-xs { - font-size: 0.75rem; - line-height: 1rem; +.font-bold { + font-weight: 700; } -.font-semibold { - font-weight: 600; +.font-medium { + font-weight: 500; } -.uppercase { - text-transform: uppercase; +.font-semibold { + font-weight: 600; } .text-gray-700 { @@ -2787,6 +3344,16 @@ input.tab:checked + .tab-content, color: rgb(55 65 81 / var(--tw-text-opacity)); } +.text-gray-900 { + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity)); +} + +.text-neutral-content { + --tw-text-opacity: 1; + color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity))); +} + .text-white { --tw-text-opacity: 1; color: rgb(255 255 255 / var(--tw-text-opacity)); @@ -2804,14 +3371,9 @@ input.tab:checked + .tab-content, box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } -.transition-colors { - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; -} - -.duration-300 { - transition-duration: 300ms; +.hover\:bg-blue-700:hover { + --tw-bg-opacity: 1; + background-color: rgb(29 78 216 / var(--tw-bg-opacity)); } .hover\:bg-gray-700:hover { @@ -2824,11 +3386,6 @@ input.tab:checked + .tab-content, border-color: rgb(96 165 250 / var(--tw-border-opacity)); } -.focus\:bg-gray-700:focus { - --tw-bg-opacity: 1; - background-color: rgb(55 65 81 / var(--tw-bg-opacity)); -} - .focus\:outline-none:focus { outline: 2px solid transparent; outline-offset: 2px; @@ -2840,15 +3397,30 @@ input.tab:checked + .tab-content, box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); } +.focus\:ring-2:focus { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + .focus\:ring-blue-300:focus { --tw-ring-opacity: 1; --tw-ring-color: rgb(147 197 253 / var(--tw-ring-opacity)); } +.focus\:ring-blue-500:focus { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity)); +} + .focus\:ring-opacity-40:focus { --tw-ring-opacity: 0.4; } +.focus\:ring-offset-2:focus { + --tw-ring-offset-width: 2px; +} + @media (prefers-color-scheme: dark) { .dark\:border-gray-600 { --tw-border-opacity: 1; @@ -2875,18 +3447,8 @@ input.tab:checked + .tab-content, color: rgb(255 255 255 / var(--tw-text-opacity)); } - .dark\:hover\:bg-gray-600:hover { - --tw-bg-opacity: 1; - background-color: rgb(75 85 99 / var(--tw-bg-opacity)); - } - .dark\:focus\:border-blue-300:focus { --tw-border-opacity: 1; border-color: rgb(147 197 253 / var(--tw-border-opacity)); } - - .dark\:focus\:bg-gray-600:focus { - --tw-bg-opacity: 1; - background-color: rgb(75 85 99 / var(--tw-bg-opacity)); - } } \ No newline at end of file diff --git a/frontend/src/lib/main.js b/frontend/src/lib/main.js index a9bb77e..3162f91 100644 --- a/frontend/src/lib/main.js +++ b/frontend/src/lib/main.js @@ -1,6 +1,6 @@ import { createViewer3D } from "./viewer" import Alpine from 'alpinejs' - +import htmx from 'htmx.org' Alpine.data('lib', () => ({ tab: '', @@ -30,8 +30,18 @@ Alpine.data('lib', () => ({ this.tab = tab }, addModel(model) { + if (this.models.includes(model)) { + return + } this.models.push(model) this.viewer.setModels(this.models.map(m => { return { id: m } })); this.tab = 'viewer' + + document.getElementsByTagName("body")[0].dispatchEvent(new CustomEvent('viewer-model-list-add', { detail: { assetID: model } })) }, + deleteModel(el, model) { + this.models = this.models.filter(m => m !== model) + this.viewer.setModels(this.models.map(m => { return { id: m } })); + el.parentElement.parentElement.remove() + } })) \ No newline at end of file diff --git a/v2/library/entities/asset.go b/v2/library/entities/asset.go index 05f6d9b..99c13dd 100644 --- a/v2/library/entities/asset.go +++ b/v2/library/entities/asset.go @@ -25,23 +25,23 @@ const ( ) type Asset struct { - ID string `query:"id" form:"id" gorm:"primaryKey"` - Label *string `query:"label" form:"label"` - Description *string `query:"description" form:"description"` - Path *string `query:"path" form:"path"` - Root *string `query:"root" form:"root"` - FSKind *string `query:"fsKind" form:"fsKind"` - FSName *string `query:"fsName" form:"fsName"` - Extension *string `query:"extension" form:"extension"` - Kind *string `query:"kind" form:"kind"` - NodeKind *NodeKind `query:"nodeKind" form:"nodeKind"` - ParentID *string `query:"parentID" form:"parentID"` - Parent *Asset `form:"-"` - NestedAssets []*Asset `query:"nestedAssets" form:"nestedAssets" gorm:"foreignKey:ParentID;constraint:OnDelete:CASCADE;"` - Thumbnail *string `query:"thumbnail" form:"thumbnail"` - SeenOnScan *bool `query:"seenOnScan" form:"seenOnScan"` - Properties Properties `query:"properties" form:"properties"` - Tags []*Tag `query:"tags" form:"tags" gorm:"many2many:asset_tags"` + ID string `query:"id" in:"form=id" gorm:"primaryKey"` + Label *string `query:"label" in:"form=label"` + Description *string `query:"description" in:"form=description"` + Path *string `query:"path" in:"form=path"` + Root *string `query:"root" in:"form=root"` + FSKind *string `query:"fsKind" in:"form=fsKind"` + FSName *string `query:"fsName" in:"form=fsName"` + Extension *string `query:"extension" in:"form=extension"` + Kind *string `query:"kind" in:"form=kind"` + NodeKind *NodeKind `query:"nodeKind" in:"form=nodeKind"` + ParentID *string `query:"parentID" in:"form=parentID"` + Parent *Asset `in:"form=-"` + NestedAssets []*Asset `query:"nestedAssets" in:"form=nestedAssets" gorm:"foreignKey:ParentID;constraint:OnDelete:CASCADE;"` + Thumbnail *string `query:"thumbnail" in:"form=thumbnail"` + SeenOnScan *bool `query:"seenOnScan" in:"form=seenOnScan"` + Properties Properties `query:"properties" in:"form=properties"` + Tags []*Tag `query:"tags" in:"form=tags" gorm:"many2many:asset_tags"` CreatedAt time.Time UpdatedAt time.Time } diff --git a/v2/library/process/process.go b/v2/library/process/process.go index bb2a2b6..46df84a 100644 --- a/v2/library/process/process.go +++ b/v2/library/process/process.go @@ -111,13 +111,13 @@ func (p *Process) Run() error { } if p.enricher != nil { - /*if err := p.enricher.Enrich(p.Asset); err != nil { + if err := p.enricher.Enrich(p.Asset); err != nil { p.enrichError = err p.enrichState = "failed" l.Error("failed to enrich asset", "error", err) } else { - }*/ - p.enrichState = "done" + p.enrichState = "done" + } } if p.renderState == "done" || p.enrichState == "done" { if err := p.p.r.SaveAsset(*p.Asset); err != nil { diff --git a/v2/library/repo/asset.go b/v2/library/repo/asset.go index 4560d9c..790fd84 100644 --- a/v2/library/repo/asset.go +++ b/v2/library/repo/asset.go @@ -2,7 +2,9 @@ package repo import ( "errors" + "fmt" "math" + "strings" "github.com/eduardooliveira/stLib/v2/database" "github.com/eduardooliveira/stLib/v2/library/entities" @@ -98,3 +100,13 @@ func (r AssetRepo) DeleteUnSeenInRoot(root string) error { func (r AssetRepo) UpdateAsset(a *entities.Asset) error { return database.DB.Model(&entities.Asset{ID: a.ID}).Updates(a).Error } + +func (r AssetRepo) SearchAsset(label string, tags string) ([]*entities.Asset, error) { + var assets []*entities.Asset + q := database.DB.Debug().Model(&entities.Asset{}).Where("label LIKE ?", fmt.Sprintf("%%%s%%", label)) + for i, t := range strings.Split(tags, ",") { + q.Joins(fmt.Sprintf("LEFT JOIN project_tags as project_tags%d on project_tags%d.project_uuid = projects.uuid", i, i)). + Where(fmt.Sprintf("project_tags%d.tag_value = ?", i), t) + } + return assets, q.Find(&assets).Error +} diff --git a/v2/library/web/comp/assetcard.templ b/v2/library/web/comp/assetcard.templ index f25621e..e02bb22 100644 --- a/v2/library/web/comp/assetcard.templ +++ b/v2/library/web/comp/assetcard.templ @@ -34,9 +34,15 @@ templ AssetCard(m *AssetCardModel) { if utils.VoZ(m.Asset.Kind) == "model" { + > + cube-outline + } } diff --git a/v2/library/web/comp/edit.templ b/v2/library/web/comp/edit.templ index 1b2fdd6..82042de 100644 --- a/v2/library/web/comp/edit.templ +++ b/v2/library/web/comp/edit.templ @@ -3,18 +3,31 @@ package comp import "github.com/eduardooliveira/stLib/v2/utils" templ Edit(m *EditModel) { -
-

Edit

-
- -
- - -
- - - -
+
+ if m!=nil { +

Edit

+
+ +
+ + +
+ + + +
+ }
if m.Action == "save" {
diff --git a/v2/library/web/comp/expand_asset_btn.templ b/v2/library/web/comp/expand_asset_btn.templ deleted file mode 100644 index 78ef16d..0000000 --- a/v2/library/web/comp/expand_asset_btn.templ +++ /dev/null @@ -1,20 +0,0 @@ -package comp - -import ( - "fmt" - "github.com/eduardooliveira/stLib/v2/library/entities" -) - -templ ExpandAssetButton(a *entities.Asset) { - //https://merakiui.com/components/application-ui/buttons - -} diff --git a/v2/library/web/comp/index.templ b/v2/library/web/comp/index.templ index 7fee909..3d025a7 100644 --- a/v2/library/web/comp/index.templ +++ b/v2/library/web/comp/index.templ @@ -15,21 +15,23 @@ templ Index(model IndexModel) {
@Pagination(model.Pagination)
- + - for _, t := range model.AssetTypes { - - } -
- -} diff --git a/v2/library/web/comp/models.go b/v2/library/web/comp/models.go index fd11343..f6317f1 100644 --- a/v2/library/web/comp/models.go +++ b/v2/library/web/comp/models.go @@ -7,11 +7,12 @@ import ( ) type IndexModel struct { - Asset *entities.Asset - AssetTypes []config.AssetType - KindFilter KindFilterModel - Main templ.Component - Pagination PaginationModel + Asset *entities.Asset + AssetTypes []config.AssetType + KindFilter KindFilterModel + Main templ.Component + Pagination PaginationModel + SearchModel SearchModel } type ListModel struct { @@ -50,3 +51,11 @@ type KindFilterModel struct { AssetTypes []config.AssetType Selected string } + +type SearchModel struct { + Search bool `in:"query=search"` + Global bool `in:"query=global"` + Parent string `in:"query=parent"` + Name string `in:"query=name"` + Tags string `in:"query=tags"` +} diff --git a/v2/library/web/comp/search.templ b/v2/library/web/comp/search.templ new file mode 100644 index 0000000..0b76b81 --- /dev/null +++ b/v2/library/web/comp/search.templ @@ -0,0 +1,50 @@ +package comp + +templ Search(model SearchModel) { +
+ +

Search

+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+} diff --git a/v2/library/web/comp/searchresult.templ b/v2/library/web/comp/searchresult.templ new file mode 100644 index 0000000..5eadc5f --- /dev/null +++ b/v2/library/web/comp/searchresult.templ @@ -0,0 +1,5 @@ +package comp + +templ SearchResult() { +
result
+} diff --git a/v2/library/web/comp/sidebar.templ b/v2/library/web/comp/sidebar.templ index 39d85d2..3383ecd 100644 --- a/v2/library/web/comp/sidebar.templ +++ b/v2/library/web/comp/sidebar.templ @@ -45,6 +45,7 @@ templ Sidebar() {
@View3D() @Details(nil) + @Edit(nil)
} diff --git a/v2/library/web/comp/view3d.templ b/v2/library/web/comp/view3d.templ index eb40ad3..6a8f6eb 100644 --- a/v2/library/web/comp/view3d.templ +++ b/v2/library/web/comp/view3d.templ @@ -1,14 +1,57 @@ package comp +import ( + "fmt" + "github.com/eduardooliveira/stLib/v2/library/entities" + "github.com/eduardooliveira/stLib/v2/utils" +) + templ View3D() {

3D Viewer

-
-
+
+
+} + +templ ViewerAssetElement(asset entities.Asset) { +
+
+
+ Tailwind-CSS-Avatar-component +
+
+
+

{ utils.VoZ(asset.Label) }

+

{ utils.VoZ(asset.Kind) }

+
+
+ +
} diff --git a/v2/library/web/details.go b/v2/library/web/details.go index d6266a4..e939638 100644 --- a/v2/library/web/details.go +++ b/v2/library/web/details.go @@ -15,17 +15,17 @@ func (h webHandler) getAssetDetailsHandler(r *http.Request) web.ResponseModel { if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return web.ResponseModel{ - S: http.StatusNotFound, - Error: err, + Status: http.StatusNotFound, + Error: err, } } return web.ResponseModel{ - S: http.StatusInternalServerError, - Error: err, + Status: http.StatusInternalServerError, + Error: err, } } return web.ResponseModel{ - S: http.StatusOK, + Status: http.StatusOK, Component: comp.Details(&comp.DetailsModel{ Asset: &asset, }), diff --git a/v2/library/web/edit.go b/v2/library/web/edit.go index f69e123..f58c7cf 100644 --- a/v2/library/web/edit.go +++ b/v2/library/web/edit.go @@ -1,51 +1,101 @@ package web import ( + "errors" "net/http" + "github.com/eduardooliveira/stLib/v2/library/entities" + "github.com/eduardooliveira/stLib/v2/library/web/comp" "github.com/eduardooliveira/stLib/v2/web" + "github.com/ggicci/httpin" + "gorm.io/gorm" ) //TODO migrate to chi -func (h webHandler) editAsset(r *http.Request) web.ResponseModel { - /* - var asset entities.Asset - var err error - var action string - - if c.Request().Method == http.MethodGet { - id := c.QueryParam("assetID") - asset, err = h.r.GetAsset(id, false) - - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return c.NoContent(http.StatusNotFound) - } - return web.Error(c, http.StatusInternalServerError, err.Error()) - } - action = "edit" - } else if c.Request().Method == http.MethodPost { - err = c.Bind(&asset) - if err != nil { - return web.Error(c, http.StatusBadRequest, err.Error()) +func (h webHandler) getEditAssetHandler(r *http.Request) web.ResponseModel { + id := r.URL.Query().Get("assetID") + asset, err := h.r.GetAsset(id, false) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return web.ResponseModel{ + Status: http.StatusNotFound, + Error: err, } - err = h.r.UpdateAsset(&asset) - if err != nil { - return web.Error(c, http.StatusInternalServerError, err.Error()) + } + return web.ResponseModel{ + Status: http.StatusInternalServerError, + Error: err, + } + } + return web.ResponseModel{ + Status: http.StatusOK, + Component: comp.Edit(&comp.EditModel{ + Asset: &asset, + }), + IsFragment: true, + } +} + +func (h webHandler) postEditAssetHandler(r *http.Request) web.ResponseModel { + + asset := r.Context().Value(httpin.Input).(*entities.Asset) + err := h.r.UpdateAsset(asset) + if err != nil { + return web.ResponseModel{ + Status: http.StatusInternalServerError, + Error: err, + } + } + return web.ResponseModel{ + Status: http.StatusOK, + Component: comp.Edit(&comp.EditModel{ + Asset: asset, + }), + IsFragment: true, + } +} + +/* +func (h webHandler) editAsset(r *http.Request) web.ResponseModel { + + var asset entities.Asset + var err error + var action string + + if c.Request().Method == http.MethodGet { + id := c.QueryParam("assetID") + asset, err = h.r.GetAsset(id, false) + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return c.NoContent(http.StatusNotFound) } - action = "save" + return web.Error(c, http.StatusInternalServerError, err.Error()) + } + action = "edit" + } else if c.Request().Method == http.MethodPost { + err = c.Bind(&asset) + if err != nil { + return web.Error(c, http.StatusBadRequest, err.Error()) + } + err = h.r.UpdateAsset(&asset) + if err != nil { + return web.Error(c, http.StatusInternalServerError, err.Error()) } - return web.Render(web.ResponseModel{ - Ctx: c, - S: http.StatusOK, - WrapperModel: corecomp.WrapperModel{ - Main: comp.Edit(&comp.EditModel{ - Asset: &asset, - Action: action, - }), - }, - IsFragment: true, - })*/ + action = "save" + } + return web.Render(web.ResponseModel{ + Ctx: c, + S: http.StatusOK, + WrapperModel: corecomp.WrapperModel{ + Main: comp.Edit(&comp.EditModel{ + Asset: &asset, + Action: action, + }), + }, + IsFragment: true, + }) return web.ResponseModel{} } +*/ diff --git a/v2/library/web/index.go b/v2/library/web/index.go index 0ae4cc6..42e7535 100644 --- a/v2/library/web/index.go +++ b/v2/library/web/index.go @@ -3,6 +3,7 @@ package web import ( "errors" "net/http" + "net/url" "path" "github.com/a-h/templ" @@ -12,66 +13,36 @@ import ( "github.com/eduardooliveira/stLib/v2/library/web/comp" "github.com/eduardooliveira/stLib/v2/utils" "github.com/eduardooliveira/stLib/v2/web" + "github.com/ggicci/httpin" "github.com/go-chi/chi/v5" "gorm.io/gorm" ) func (h webHandler) indexHandler(r *http.Request) web.ResponseModel { - var err error var asset entities.Asset - if chi.URLParam(r, "assetID") != "" { - asset, err = h.r.GetAsset(chi.URLParam(r, "assetID"), true) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return web.ResponseModel{ - S: http.StatusNotFound, - Error: err, - } - } - return web.ResponseModel{ - S: http.StatusInternalServerError, - Error: err, - } + var resp *web.ResponseModel + + if r.URL.Query().Has("search") { + return doSearch(h, r) + } else if chi.URLParam(r, "assetID") != "" { + asset, resp = withAssetID(h, r, chi.URLParam(r, "assetID")) + if resp != nil { + return *resp } } else { - if len(config.Cfg.Library.FileSystems) == 0 || (len(config.Cfg.Library.FileSystems) == 1 && config.Cfg.Library.FileSystems[0].Path == "change_me") { - return web.ResponseModel{ - S: http.StatusNotFound, - Error: errors.New("No library paths configured, check library.paths in config.toml"), - } - } - roots, err := h.r.GetAssetRoots(true) - if err != nil { - return web.ResponseModel{ - S: http.StatusInternalServerError, - Error: err, - } - } - if len(roots) > 1 { - asset = entities.Asset{ - ID: "", - Label: utils.Ptr("Libraries"), - NestedAssets: roots, - } - } else { - asset = utils.VoZ(roots[0]) - } - } - - if err != nil { - return web.ResponseModel{ - S: http.StatusInternalServerError, - Error: err, + asset, resp = roots(h, r) + if resp != nil { + return *resp } } if asset.ID != "" { - err = h.r.LoadParents(&asset, 5, "ID", "Label") + err := h.r.LoadParents(&asset, 5, "ID", "Label") if err != nil { return web.ResponseModel{ - S: http.StatusInternalServerError, - Error: err, + Status: http.StatusInternalServerError, + Error: err, } } } @@ -82,12 +53,12 @@ func (h webHandler) indexHandler(r *http.Request) web.ResponseModel { }) if err != nil { return web.ResponseModel{ - S: http.StatusInternalServerError, - Error: err, + Status: http.StatusInternalServerError, + Error: err, } } + return web.ResponseModel{ - S: http.StatusOK, PushState: path.Join("/lib", asset.ID), Component: maybeWrapComponent(r, comp.Index(comp.IndexModel{ Asset: &asset, @@ -107,3 +78,83 @@ func maybeWrapComponent(r *http.Request, c templ.Component) templ.Component { } return c } + +func withAssetID(h webHandler, r *http.Request, assetID string) (entities.Asset, *web.ResponseModel) { + asset, err := h.r.GetAsset(assetID, true) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return entities.Asset{}, &web.ResponseModel{ + Status: http.StatusNotFound, + Error: errors.New("Asset not found"), + } + } + return entities.Asset{}, &web.ResponseModel{ + Status: http.StatusInternalServerError, + Error: err, + } + } + return asset, nil +} + +func doSearch(h webHandler, r *http.Request) web.ResponseModel { + pushParams := r.URL.Query() + pushURL := url.URL{ + Path: "/lib", + RawQuery: pushParams.Encode(), + } + req := r.Context().Value(httpin.Input).(*comp.SearchModel) + + if req == nil || !req.Search { + return web.ResponseModel{ + Status: http.StatusBadRequest, + Error: errors.New("missing search query"), + } + } + assets, err := h.r.SearchAsset(req.Name, req.Tags) + if err != nil { + return web.ResponseModel{ + Status: http.StatusInternalServerError, + Error: err, + } + } + asset := &entities.Asset{ + ID: "", + Label: utils.Ptr("Search results"), + NestedAssets: assets, + } + return web.ResponseModel{ + PushState: pushURL.String(), + Component: maybeWrapComponent(r, comp.Index(comp.IndexModel{ + Asset: asset, + SearchModel: *req, + Main: comp.List(comp.ListModel{Asset: asset}), + })), + } +} + +func roots(h webHandler, r *http.Request) (entities.Asset, *web.ResponseModel) { + if len(config.Cfg.Library.FileSystems) == 0 || (len(config.Cfg.Library.FileSystems) == 1 && config.Cfg.Library.FileSystems[0].Path == "change_me") { + return entities.Asset{}, &web.ResponseModel{ + Status: http.StatusNotFound, + Error: errors.New("No library paths configured, check library.paths in config.toml"), + } + } + roots, err := h.r.GetAssetRoots(true) + if err != nil { + return entities.Asset{}, &web.ResponseModel{ + Status: http.StatusInternalServerError, + Error: err, + } + } + var asset entities.Asset + if len(roots) > 1 { + asset = entities.Asset{ + ID: "", + Label: utils.Ptr("Libraries"), + NestedAssets: roots, + } + } else { + asset = utils.VoZ(roots[0]) + } + return asset, nil +} diff --git a/v2/library/web/list.go b/v2/library/web/list.go index 30f7292..d16c8fb 100644 --- a/v2/library/web/list.go +++ b/v2/library/web/list.go @@ -68,29 +68,29 @@ func (h webHandler) listHandler(r *http.Request) web.ResponseModel { var asset entities.Asset if r.URL.Query().Get("assetID") == "" { return web.ResponseModel{ - S: http.StatusBadRequest, - Error: errors.New("Asset ID is required"), + Status: http.StatusBadRequest, + Error: errors.New("Asset ID is required"), } } asset, err = h.r.GetAsset(r.URL.Query().Get("assetID"), true) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return web.ResponseModel{ - S: http.StatusNotFound, - Error: err, + Status: http.StatusNotFound, + Error: err, } } return web.ResponseModel{ - S: http.StatusInternalServerError, - Error: err, + Status: http.StatusInternalServerError, + Error: err, } } err = h.r.LoadParents(&asset, 5, "ID", "Label") if err != nil { return web.ResponseModel{ - S: http.StatusInternalServerError, - Error: err, + Status: http.StatusInternalServerError, + Error: err, } } @@ -101,16 +101,16 @@ func (h webHandler) listHandler(r *http.Request) web.ResponseModel { if err != nil { return web.ResponseModel{ - S: http.StatusInternalServerError, - Error: err, + Status: http.StatusInternalServerError, + Error: err, } } u, err := url.Parse(r.URL.String()) if err != nil { return web.ResponseModel{ - S: http.StatusInternalServerError, - Error: err, + Status: http.StatusInternalServerError, + Error: err, } } @@ -121,7 +121,7 @@ func (h webHandler) listHandler(r *http.Request) web.ResponseModel { pgModel.OOB = true return web.ResponseModel{ - S: http.StatusOK, + Status: http.StatusOK, Component: listComp, IsFragment: true, PushState: u.String(), diff --git a/v2/library/web/new.go b/v2/library/web/new.go index 2aeef1a..d359152 100644 --- a/v2/library/web/new.go +++ b/v2/library/web/new.go @@ -37,8 +37,8 @@ func (h webHandler) newAssetHandler(r *http.Request) web.ResponseModel { if req.ParentID == "" { return web.ResponseModel{ - Error: errors.New("missing parentID"), - S: http.StatusBadRequest, + Error: errors.New("missing parentID"), + Status: http.StatusBadRequest, } } @@ -46,13 +46,13 @@ func (h webHandler) newAssetHandler(r *http.Request) web.ResponseModel { if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return web.ResponseModel{ - Error: err, - S: http.StatusNotFound, + Error: err, + Status: http.StatusNotFound, } } return web.ResponseModel{ - Error: err, - S: http.StatusInternalServerError, + Error: err, + Status: http.StatusInternalServerError, } } model.ParentID = req.ParentID @@ -64,20 +64,20 @@ func (h webHandler) newAssetHandler(r *http.Request) web.ResponseModel { } if req.Mode == "" { return web.ResponseModel{ - Error: errors.New("missing mode"), - S: http.StatusBadRequest, + Error: errors.New("missing mode"), + Status: http.StatusBadRequest, } } if _, ok := reqHandlers[req.Mode]; !ok { return web.ResponseModel{ - Error: errors.New("invalid mode"), - S: http.StatusBadRequest, + Error: errors.New("invalid mode"), + Status: http.StatusBadRequest, } } if err, s := reqHandlers[req.Mode](r, parent, *req); err != nil { return web.ResponseModel{ - Error: err, - S: s, + Error: err, + Status: s, } } @@ -90,8 +90,8 @@ func (h webHandler) newAssetHandler(r *http.Request) web.ResponseModel { entries, err := os.ReadDir(filepath.Join(config.Cfg.Core.DataFolder, "temp")) if err != nil { return web.ResponseModel{ - Error: err, - S: http.StatusInternalServerError, + Error: err, + Status: http.StatusInternalServerError, } } @@ -99,7 +99,7 @@ func (h webHandler) newAssetHandler(r *http.Request) web.ResponseModel { model.TempFiles = append(model.TempFiles, e.Name()) } return web.ResponseModel{ - S: http.StatusOK, + Status: http.StatusOK, Component: comp.New(model), Events: events, IsFragment: true, diff --git a/v2/library/web/sidebar.go b/v2/library/web/sidebar.go deleted file mode 100644 index 51a315b..0000000 --- a/v2/library/web/sidebar.go +++ /dev/null @@ -1,16 +0,0 @@ -package web - -import ( - "net/http" - - "github.com/eduardooliveira/stLib/v2/library/web/comp" - "github.com/eduardooliveira/stLib/v2/web" -) - -func (h webHandler) sidebarHandler(r *http.Request) web.ResponseModel { - return web.ResponseModel{ - S: http.StatusOK, - Component: comp.Sidebar(), - IsFragment: true, - } -} diff --git a/v2/library/web/viewer.go b/v2/library/web/viewer.go new file mode 100644 index 0000000..05a03c9 --- /dev/null +++ b/v2/library/web/viewer.go @@ -0,0 +1,38 @@ +package web + +import ( + "errors" + "net/http" + + "github.com/eduardooliveira/stLib/v2/library/web/comp" + "github.com/eduardooliveira/stLib/v2/web" + "gorm.io/gorm" +) + +func (h webHandler) viewerListAsset(r *http.Request) web.ResponseModel { + id := r.URL.Query().Get("assetID") + if id == "" { + return web.ResponseModel{ + Status: http.StatusBadRequest, + Error: errors.New("Asset ID is required"), + } + } + asset, err := h.r.GetAsset(id, false) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return web.ResponseModel{ + Status: http.StatusNotFound, + Error: err, + } + } + return web.ResponseModel{ + Status: http.StatusInternalServerError, + Error: err, + } + } + return web.ResponseModel{ + Status: http.StatusOK, + Component: comp.ViewerAssetElement(asset), + IsFragment: true, + } +} diff --git a/v2/library/web/web.go b/v2/library/web/web.go index e7e4c13..bd34aae 100644 --- a/v2/library/web/web.go +++ b/v2/library/web/web.go @@ -4,8 +4,10 @@ import ( "log/slog" "net/http" + "github.com/eduardooliveira/stLib/v2/library/entities" "github.com/eduardooliveira/stLib/v2/library/process" "github.com/eduardooliveira/stLib/v2/library/repo" + "github.com/eduardooliveira/stLib/v2/library/web/comp" "github.com/eduardooliveira/stLib/v2/web" "github.com/ggicci/httpin" "github.com/go-chi/chi/v5" @@ -24,14 +26,18 @@ func New(repo *repo.AssetRepo, p *process.Processor) (http.Handler, error) { p: p, } r := chi.NewRouter() - r.Get("/", web.R(wh.indexHandler)) + r.With(httpin.NewInput(comp.SearchModel{})).Get("/", web.R(wh.indexHandler)) r.Get("/{assetID}", web.R(wh.indexHandler)) - r.Get("/sidebar", web.R(wh.sidebarHandler)) + //r.Get("/sidebar", web.R(web.RenderFragment(comp.Sidebar()))) + r.Get("/viewer/list", web.R(wh.viewerListAsset)) r.Get("/list", web.R(wh.listHandler)) r.Get("/{assetID}/file", wh.getFileHandler) r.Get("/details", web.R(wh.getAssetDetailsHandler)) + r.Get("/edit", web.R(wh.getEditAssetHandler)) + r.With(httpin.NewInput(entities.Asset{})).Post("/edit", web.R(wh.postEditAssetHandler)) + r.Get("/new", web.R(wh.newAssetHandler)) r.With(httpin.NewInput(newAssetRequest{})).Post("/new", web.R(wh.newAssetHandler)) diff --git a/v2/web/index.go b/v2/web/index.go index acc4b42..3fd397b 100644 --- a/v2/web/index.go +++ b/v2/web/index.go @@ -9,7 +9,7 @@ import ( func (h webHandler) indexHandler(r *http.Request) ResponseModel { return ResponseModel{ - S: http.StatusOK, + Status: http.StatusOK, Component: comp.WrapperComponent(comp.WrapperModel{ Main: templ.NopComponent, }), diff --git a/v2/web/web.go b/v2/web/web.go index 8ac3096..fadf102 100644 --- a/v2/web/web.go +++ b/v2/web/web.go @@ -35,3 +35,13 @@ func RenderIndex(path string) error { }).Render(context.Background(), f) } + +func RenderFragment(frag templ.Component) func(r *http.Request) ResponseModel { + return func(r *http.Request) ResponseModel { + return ResponseModel{ + Status: http.StatusOK, + IsFragment: true, + Component: frag, + } + } +} diff --git a/v2/web/writter.go b/v2/web/writter.go index 3219c78..8dc0cfc 100644 --- a/v2/web/writter.go +++ b/v2/web/writter.go @@ -7,19 +7,16 @@ import ( "github.com/a-h/templ" "github.com/eduardooliveira/stLib/v2/web/comp" - "github.com/labstack/echo/v4" ) type ResponseModel struct { - Ctx echo.Context - S int - Error error - Component templ.Component - OOB []templ.Component - WrapperModel comp.WrapperModel - IsFragment bool - PushState string - Events []string + Status int + Error error + Component templ.Component + OOB []templ.Component + IsFragment bool + PushState string + Events []string } type RenderRequest func(r *http.Request) ResponseModel @@ -46,8 +43,10 @@ func R(rr RenderRequest) http.HandlerFunc { if rm.Events != nil && len(rm.Events) > 0 { w.Header().Add("HX-Trigger", strings.Join(rm.Events, ", ")) } - - w.WriteHeader(rm.S) + if rm.Status == 0 { + rm.Status = http.StatusOK + } + w.WriteHeader(rm.Status) fComponent := rm.Component if !isFragment { fComponent = comp.WrapperComponent(comp.WrapperModel{Main: rm.Component})