From 43f8ac0792d2fdf18efdaceec632f99d130ead35 Mon Sep 17 00:00:00 2001 From: karrot0 Date: Wed, 5 Nov 2025 21:34:04 -0500 Subject: [PATCH 1/3] feat: Added Extensions to the Extension Route and Extension Details --- .../theme/components/ExtensionDetails.vue | 218 +++ .../theme/components/ExtensionList.css | 1546 +++++++++++++++++ .../theme/components/ExtensionList.vue | 800 +++++++++ .../theme/components/ExtensionSearch.vue | 223 +++ src/.vitepress/theme/index.ts | 8 +- src/.vitepress/theme/lib/extensions.ts | 380 ++++ src/extension-list.md | 4 +- 7 files changed, 3177 insertions(+), 2 deletions(-) create mode 100644 src/.vitepress/theme/components/ExtensionDetails.vue create mode 100644 src/.vitepress/theme/components/ExtensionList.css create mode 100644 src/.vitepress/theme/components/ExtensionList.vue create mode 100644 src/.vitepress/theme/components/ExtensionSearch.vue create mode 100644 src/.vitepress/theme/lib/extensions.ts diff --git a/src/.vitepress/theme/components/ExtensionDetails.vue b/src/.vitepress/theme/components/ExtensionDetails.vue new file mode 100644 index 0000000..e8fed64 --- /dev/null +++ b/src/.vitepress/theme/components/ExtensionDetails.vue @@ -0,0 +1,218 @@ + + + diff --git a/src/.vitepress/theme/components/ExtensionList.css b/src/.vitepress/theme/components/ExtensionList.css new file mode 100644 index 0000000..06817bf --- /dev/null +++ b/src/.vitepress/theme/components/ExtensionList.css @@ -0,0 +1,1546 @@ +.extension-list { + margin: 2rem 0; +} +.loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1rem; + padding: 3rem 1rem; + color: var(--vp-c-text-2); + text-align: center; +} +.spinner { + width: 24px; + height: 24px; + border: 3px solid var(--vp-c-bg-soft); + border-top: 3px solid var(--vp-c-brand); + border-radius: 50%; + animation: spin 1s linear infinite; +} +@keyframes spin { + 0% { + transform: rotate(0deg); +} +100% { + transform: rotate(360deg); +} +}.error { + padding: 1rem; + background: var(--vp-c-danger-soft); + border: 1px solid var(--vp-c-danger); + border-radius: 8px; + color: var(--vp-c-danger); +} +.search-section { + background: #18181b; + border: 1px solid var(--vp-c-border); + border-radius: 12px; + padding: 1.25rem; + display: flex; + flex-direction: column; + gap: 1rem; + margin-bottom: 2rem; +} +.search-bar { + width: 100%} +.search-input-container { + position: relative; + display: flex; + align-items: center; +} +.search-icon { + position: absolute; + left: 1rem; + color: var(--vp-c-text-2); + pointer-events: none; + display: flex; +} +.search-input { + width: 100%; + padding: .75rem 1rem .75rem 2.75rem; + background: var(--vp-c-bg-soft); + border: 1px solid #fff0; + border-radius: 8px; + color: var(--vp-c-text-1); + font-size: .95rem; + transition: all 0.2s ease; +} +.search-input:focus { + outline: none; + background: var(--vp-c-bg); + border-color: var(--vp-c-brand); + box-shadow: 0 0 0 2px var(--vp-c-brand-soft); +} +.search-input::-webkit-search-cancel-button { + display: none; +} +.clear-btn { + position: absolute; + right: .5rem; + background: none; + border: none; + color: var(--vp-c-text-2); + font-size: 1.2rem; + cursor: pointer; + padding: .5rem; + border-radius: 4px; + transition: all 0.2s ease; + min-width: 36px; + min-height: 36px; + display: flex; + align-items: center; + justify-content: center; +} +.clear-btn:hover { + background: var(--vp-c-bg); + color: var(--vp-c-text-1); +} +.search-shortcut { + display: flex; + gap: .25rem; + align-items: center; + position: absolute; + right: .75rem; +} +.search-shortcut kbd { + padding: .2rem .4rem; + background: var(--vp-c-bg); + border: 1px solid var(--vp-c-border); + border-radius: 4px; + font-size: .7rem; + color: var(--vp-c-text-2); + font-family: inherit; +} +.filters-row { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + flex-wrap: wrap; +} +.select-group { + display: flex; + gap: .75rem; + flex: 1; + min-width: 280px; +} +.select-wrapper { + position: relative; + flex: 1; +} +.custom-select { + width: 100%; + appearance: none; + -webkit-appearance: none; + background: var(--vp-c-bg-soft); + border: 1px solid #fff0; + border-radius: 8px; + padding: .6rem 2rem .6rem 1rem; + color: var(--vp-c-text-1); + font-size: .9rem; + cursor: pointer; + transition: all 0.2s ease; +} +.custom-select:hover { + background: var(--vp-c-bg); + border-color: var(--vp-c-border); +} +.custom-select:focus { + outline: none; + border-color: var(--vp-c-brand); +} +.select-arrow { + position: absolute; + right: .8rem; + top: 50%; + transform: translateY(-50%); + font-size: .7rem; + color: var(--vp-c-text-2); + pointer-events: none; +} +.toggles-group { + display: flex; + gap: .75rem; +} +.toggle-btn { + display: flex; + align-items: center; + padding: .6rem 1rem; + background: var(--vp-c-bg-soft); + border: 1px solid #fff0; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s ease; + user-select: none; + color: var(--vp-c-text-2); +} +.toggle-btn:hover { + background: var(--vp-c-bg); + border-color: var(--vp-c-border); + color: var(--vp-c-text-1); +} +.toggle-btn.active { + background: var(--vp-c-danger-soft); + color: var(--vp-c-danger); + border-color: var(--vp-c-danger-soft); +} +.hidden-checkbox { + display: none; +} +.toggle-label { + font-size: .9rem; + font-weight: 500; +} +.action-btn { + display: flex; + align-items: center; + gap: .5rem; + padding: .6rem 1rem; + background: var(--vp-c-bg-soft); + border: 1px solid #fff0; + border-radius: 8px; + color: var(--vp-c-text-1); + font-size: .9rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} +.action-btn:hover { + background: var(--vp-c-bg); + border-color: var(--vp-c-border); +} +.action-btn.active { + background: var(--vp-c-brand); + color: #fff; +} +.filter-count { + background: rgb(255 255 255 / .25); + padding: .1rem .4rem; + border-radius: 10px; + font-size: .75rem; + min-width: 1.2rem; + text-align: center; +} +.expand-icon { + font-size: .65rem; + transition: transform 0.2s ease; +} +.expand-icon.expanded { + transform: rotate(180deg); +} +.btn-icon { + font-size: 1rem; +} +@media (max-width:640px) { + .filters-row { + flex-direction: column; + align-items: stretch; +} +.select-group { + flex-direction: column; +} +.toggles-group { + justify-content: space-between; +} +.toggle-btn, .action-btn { + flex: 1; + justify-content: center; +} +}.results-summary { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + padding: .75rem 0; + border-bottom: 1px solid var(--vp-c-border); +} +.results-count { + font-size: .9rem; + color: var(--vp-c-text-2); +} +.active-filters { + display: flex; + gap: .5rem; + flex-wrap: wrap; +} +.filter-tag { + padding: .25rem .5rem; + background: var(--vp-c-brand-soft); + color: var(--vp-c-brand); + border-radius: 4px; + font-size: .8rem; + font-weight: 500; +} +.filter-tag.nsfw-active { + background: var(--vp-c-danger-soft); + color: var(--vp-c-danger); +} +.extensions-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1.5rem; + margin: 1.5rem 0; +} +.extension-card { + padding: 1.5rem; + border: 1px solid var(--vp-c-border); + border-radius: 12px; + background: #18181b; + color: #fff; + transition: all 0.3s ease; + cursor: pointer; + position: relative; + display: flex; + flex-direction: column; + height: 100%} +.extension-card:hover { + border-color: var(--vp-c-brand); + box-shadow: 0 4px 12px rgb(0 0 0 / .1); +} +.extension-card:active { + transform: scale(.98); + transition: transform 0.1s ease; +} +.extension-card.selected { + border-color: var(--vp-c-brand); + box-shadow: 0 0 0 3px var(--vp-c-brand-soft); + background: var(--vp-c-brand-dimm); +} +.extension-card.nsfw-blur { + position: relative; + overflow: hidden; + background: #18181b; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + min-height: 200px; + background-repeat: no-repeat; + background-attachment: scroll; + transition: all 0.6s cubic-bezier(.4, 0, .2, 1); +} +.extension-card.nsfw-blur::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(135deg, rgb(0 0 0 / .03) 0%, rgb(0 0 0 / .15) 100%); + backdrop-filter: blur(30px); + z-index: 1; + transition: all 0.6s cubic-bezier(.4, 0, .2, 1); +} +.extension-card.nsfw-blur .extension-header, .extension-card.nsfw-blur .extension-meta, .extension-card.nsfw-blur .extension-actions, .extension-card.nsfw-blur .selected-indicator { + opacity: 0; +} +.extension-header { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 1rem; + padding-bottom: 1rem; + border-bottom: 1px solid var(--vp-c-divider); + transition: opacity 0.6s cubic-bezier(.4, 0, .2, 1); +} +.extension-icon { + width: 56px; + height: 56px; + border-radius: 12px; + object-fit: cover; + flex-shrink: 0; + box-shadow: 0 2px 8px rgb(0 0 0 / .1); +} +.extension-header-text { + flex: 1; + display: flex; + flex-direction: column; + gap: 0; + min-width: 0; +} +.extension-name { + margin: 0!important; + font-size: 1.1rem; + font-weight: 600; + color: #fff; + line-height: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.extension-source { + margin: 0; +} +.source-badge { + padding: .2rem .4rem; + border-radius: 6px; + font-size: .7rem; + font-weight: 500; + text-transform: uppercase; +} +.source-badge.inkdex { + background: var(--vp-c-brand-soft); + color: var(--vp-c-brand); +} +.source-badge.karrot { + background: var(--vp-c-warning-soft); + color: var(--vp-c-warning); +} +.extension-meta { + margin-bottom: 1rem; + transition: opacity 0.6s cubic-bezier(.4, 0, .2, 1); +} +.extension-description { + margin-bottom: .5rem; + color: #fff; + font-size: .9rem; + line-height: 1.4; + display: -webkit-box; + line-clamp: 2; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} +.content-rating { + margin-bottom: .5rem; +} +.rating-badge { + display: inline-block; + padding: .05rem .25rem; + border-radius: 6px; + font-size: .7rem; + font-weight: 600; + text-transform: uppercase; +} +.header-badges { + display: flex; + gap: .5rem; + margin-top: .25rem; + align-items: center; +} +.version-badge { + display: inline-block; + padding: .05rem .3rem; + border-radius: 6px; + font-size: .7rem; + font-weight: 500; + background: var(--vp-c-bg-soft); + color: var(--vp-c-text-2); + border: 1px solid var(--vp-c-border); +} +.extension-badges { + display: flex; + flex-wrap: wrap; + gap: .25rem; + margin-bottom: .5rem; +} +.genre-badge { + padding: .1rem .3rem; + border-radius: 8px; + font-size: .65rem; + font-weight: 500; + white-space: nowrap; +} +.extension-language { + margin-bottom: .5rem; +} +.language-badge { + padding: .1rem .3rem; + border-radius: 8px; + font-size: .65rem; + font-weight: 500; + background: var(--vp-c-bg); + color: var(--vp-c-text-2); + border: 1px solid var(--vp-c-border); +} +.extension-actions { + display: flex; + gap: .5rem; + margin-top: auto; + transition: opacity 0.6s cubic-bezier(.4, 0, .2, 1); +} +.details-btn { + padding: .75rem 1rem; + border-radius: 4px; + text-decoration: none; + font-size: .9rem; + font-weight: 500; + transition: all 0.2s ease; + display: inline-block; + text-align: center; + flex: 1; + background: #fff0; + color: var(--vp-c-text-2); + border: 1px solid var(--vp-c-border); + cursor: pointer; + min-height: 44px; +} +.details-btn:hover { + background: var(--vp-c-bg-soft); + border-color: var(--vp-c-text-2); + color: var(--vp-c-text-1); +} +.details-btn:active { + background: var(--vp-c-bg); + transform: scale(.98); +} +.selected-indicator { + margin-top: .5rem; + padding: .5rem; + background: var(--vp-c-brand-soft); + border-radius: 4px; + text-align: center; +} +.selected-checkmark { + color: var(--vp-c-brand); + font-weight: 600; + font-size: .9rem; +} +.floating-install-btn { + position: fixed; + bottom: 2rem; + right: 2rem; + z-index: 100; +} +.install-selected-btn { + display: flex; + align-items: center; + gap: .75rem; + padding: 1rem 1.5rem; + background: var(--vp-c-brand); + color: #fff; + border: none; + border-radius: 50px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + box-shadow: 0 4px 12px rgb(0 0 0 / .2); + transition: all 0.3s ease; + min-height: 48px; + min-width: 48px; +} +.install-selected-btn:hover { + background: var(--vp-c-brand-dark); + box-shadow: 0 6px 16px rgb(0 0 0 / .3); +} +.install-selected-btn:active { + transform: scale(.95); + box-shadow: 0 2px 8px rgb(0 0 0 / .2); +} +.selected-count { + background: rgb(255 255 255 / .25); + padding: .25rem .6rem; + border-radius: 50px; + font-size: .9rem; +} +.no-results { + text-align: center; + padding: 3rem 1rem; + color: var(--vp-c-text-2); +} +.no-results-icon { + font-size: 3rem; + margin-bottom: 1rem; +} +.no-results h3 { + margin: 0 0 .5rem 0; + color: var(--vp-c-text-1); +} +.no-results p { + margin: 0 0 1.5rem 0; +} +.clear-search-btn { + padding: .75rem 1rem; + background: var(--vp-c-brand); + color: #fff; + border: 1px solid var(--vp-c-brand); + border-radius: 4px; + cursor: pointer; + font-size: .9rem; + transition: all 0.2s ease; + min-height: 44px; +} +.clear-search-btn:hover { + background: var(--vp-c-brand-dark); + border-color: var(--vp-c-brand-dark); +} +.clear-search-btn:active { + transform: scale(.98); +} +.stats { + margin-top: 2rem; + padding: 1rem; + background: var(--vp-c-bg-soft); + border-radius: 8px; + text-align: center; + color: var(--vp-c-text-2); + font-size: .9rem; +} +@media (max-width:1200px) { + .extensions-grid { + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1.25rem; +} +}@media (max-width:1024px) { + .extensions-grid { + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 1rem; +} +}@media (max-width:768px) { + .search-section { + padding: 1rem; +} +.filter-section { + flex-direction: column; + align-items: flex-start; + gap: .75rem; +} +.nsfw-toggle { + margin-top: .25rem; +} +.results-summary { + flex-direction: column; + align-items: flex-start; + gap: .5rem; +} +.extensions-grid { + grid-template-columns: repeat(2, 1fr); + gap: .75rem; +} +.extension-card { + padding: 1rem; +} +.extension-card.nsfw-blur { + min-height: 160px; +} +.extension-header { + flex-direction: column; + align-items: center; + text-align: center; + gap: .5rem; +} +.extension-icon { + width: 48px; + height: 48px; +} +.extension-header-text { + align-items: center; +} +.extension-name { + font-size: .9rem; +} +.source-badge { + font-size: .65rem; + padding: .2rem .4rem; +} +.extension-badges { + justify-content: center; +} +.genre-badge { + font-size: .6rem; + padding: .1rem .25rem; + border-radius: 10px; +} +.rating-badge { + font-size: .65rem; + padding: .1rem .25rem; + border-radius: 8px; +} +.extension-actions { + margin-top: auto; +} +.details-btn { + font-size: .8rem; + padding: .4rem .75rem; + min-height: 40px; +} +.floating-install-btn { + bottom: 1.5rem; + right: 1.5rem; + left: 1.5rem; + width: calc(100vw - 3rem); + max-width: none; +} +.install-selected-btn { + padding: 1rem 1.5rem; + font-size: .95rem; + min-height: 48px; + width: 100%; + justify-content: center; +} +}@media (max-width:480px) { + .extensions-grid { + grid-template-columns: 1fr; + gap: .5rem; +} +.extension-card { + padding: .75rem; +} +.extension-icon { + width: 40px; + height: 40px; +} +.extension-name { + font-size: .85rem; +} +.details-btn { + font-size: .75rem; + padding: .35rem .6rem; +} +}.details-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgb(0 0 0 / .5); + backdrop-filter: blur(4px); + z-index: 1000; +} +.details-overlay.desktop { + background: rgb(0 0 0 / .3); +} +.details-container { + position: absolute; + bottom: 0; + left: 0; + right: 0; + max-height: 80vh; + background: var(--vp-c-bg); + border-radius: 20px 20px 0 0; + box-shadow: 0 -4px 20px rgb(0 0 0 / .3); + overflow-y: auto; + overscroll-behavior: contain; +} +.details-container.desktop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: auto; + width: 400px; + max-width: 90vw; + max-height: 100vh; + border-radius: 0; + border-left: 1px solid var(--vp-c-border); +} +.details-handle { + display: flex; + justify-content: center; + padding: 12px; + border-bottom: 1px solid var(--vp-c-border); +} +.handle-bar { + width: 40px; + height: 4px; + background: var(--vp-c-border); + border-radius: 2px; +} +.details-close-btn { + position: absolute; + top: 16px; + right: 16px; + background: none; + border: none; + color: var(--vp-c-text-2); + cursor: pointer; + padding: 12px; + border-radius: 6px; + transition: all 0.2s ease; + z-index: 10; + min-width: 44px; + min-height: 44px; + display: flex; + align-items: center; + justify-content: center; +} +.details-close-btn:hover { + background: var(--vp-c-bg-soft); + color: var(--vp-c-text-1); +} +.details-close-btn:active { + background: var(--vp-c-bg); + transform: scale(.95); +} +.details-content { + padding: 1.5rem; + padding-bottom: 2rem; +} +.details-content.desktop { + padding: 2rem; + padding-top: 3rem; + height: 100%; + display: flex; + flex-direction: column; +} +.details-header { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 1.5rem; + padding-bottom: 1rem; + border-bottom: 1px solid var(--vp-c-divider); +} +.details-icon { + width: 64px; + height: 64px; + border-radius: 16px; + object-fit: cover; + flex-shrink: 0; + box-shadow: 0 2px 8px rgb(0 0 0 / .1); +} +.details-header-text { + flex: 1; +} +.details-title { + margin: 0 0 .5rem 0; + font-size: 1.5rem; + font-weight: 600; + color: var(--vp-c-text-1); +} +.details-source-badge { + padding: .25rem .75rem; + border-radius: 6px; + font-size: .8rem; + font-weight: 500; + text-transform: uppercase; +} +.details-source-badge.inkdex { + background: var(--vp-c-brand-soft); + color: var(--vp-c-brand); +} +.details-source-badge.karrot { + background: var(--vp-c-warning-soft); + color: var(--vp-c-warning); +} +.details-meta { + display: grid; + gap: 1rem; + margin-bottom: 1.5rem; +} +.details-section h3 { + margin: 0 0 .5rem 0; + font-size: 1rem; + font-weight: 600; + color: var(--vp-c-text-1); +} +.details-rating-badge { + padding: .5rem 1rem; + border-radius: 8px; + font-size: .9rem; + font-weight: 600; + text-transform: uppercase; + display: inline-block; +} +.details-language-badge, .details-version-badge { + padding: .25rem .75rem; + border-radius: 6px; + font-size: .9rem; + background: var(--vp-c-bg-soft); + color: var(--vp-c-text-2); + border: 1px solid var(--vp-c-border); + display: inline-block; +} +.details-badges { + display: flex; + flex-wrap: wrap; + gap: .5rem; +} +.details-genre-badge { + padding: .2rem .6rem; + border-radius: 8px; + font-size: .75rem; + font-weight: 500; + white-space: nowrap; +} +.details-developers { + display: flex; + flex-direction: column; + gap: .75rem; +} +.details-developer { + display: flex; + align-items: center; + gap: .75rem; + flex-wrap: wrap; +} +.dev-name { + font-weight: 500; + color: var(--vp-c-text-1); +} +.dev-link { + font-size: .8rem; + color: var(--vp-c-brand); + text-decoration: none; + padding: .2rem .5rem; + border-radius: 4px; + background: var(--vp-c-brand-soft); + transition: all 0.2s ease; +} +.dev-link:hover { + background: var(--vp-c-brand); + color: #fff; +} +.details-description { + margin-bottom: 2rem; + padding: 1rem; + background: var(--vp-c-bg-soft); + border-radius: 8px; + border: 1px solid var(--vp-c-border); +} +.details-description h3 { + margin: 0 0 .75rem 0; + font-size: 1.1rem; + color: var(--vp-c-text-1); +} +.details-description p { + margin: 0; + color: var(--vp-c-text-2); + line-height: 1.6; +} +.details-actions { + display: flex; + justify-content: center; + padding-top: 1rem; + border-top: 1px solid var(--vp-c-border); + margin-top: auto; +} +.details-install-btn { + padding: 1rem 2rem; + background: var(--vp-c-brand); + color: #fff; + border: none; + border-radius: 50px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + box-shadow: 0 4px 12px rgb(0 0 0 / .2); + transition: all 0.3s ease; + min-height: 48px; + width: 100%; + max-width: 300px; +} +.details-install-btn:hover { + background: var(--vp-c-brand-dark); + box-shadow: 0 4px 12px rgb(0 0 0 / .2); +} +.details-install-btn:active { + transform: scale(.98); + box-shadow: 0 2px 8px rgb(0 0 0 / .2); +} +@media (max-width:767px) { + .details-container.desktop { + position: absolute; + bottom: 0; + left: 0; + right: 0; + top: auto; + width: auto; + max-width: none; + max-height: 80vh; + border-radius: 20px 20px 0 0; + border-left: none; +} +.details-content.desktop { + padding: 1.5rem; + padding-bottom: 2rem; + padding-top: 1.5rem; + height: auto; + display: block; +} +.details-close-btn { + display: none; +} +.details-handle { + display: flex; +} +.details-install-btn { + padding: 1rem 1.5rem; + font-size: .95rem; + min-height: 48px; + max-width: none; +} +}@media (max-width:360px) { + .extensions-grid { + grid-template-columns: 1fr; + gap: .5rem; +} +.extension-card { + padding: .5rem; +} +.extension-card.nsfw-blur { + min-height: 140px; +} +.extension-icon { + width: 36px; + height: 36px; +} +.extension-name { + font-size: .8rem; +} +.search-section { + padding: .75rem; +} +.search-input { + font-size: .9rem; + padding: .6rem .8rem; +} +.clear-btn { + min-width: 40px; + min-height: 40px; + padding: .5rem; +} +.rating-filter { + font-size: .85rem; + padding: .6rem .5rem; + min-height: 40px; +} +.nsfw-label { + font-size: .85rem; +} +.nsfw-checkbox { + width: 16px; + height: 16px; +} +.details-btn { + font-size: .7rem; + padding: .3rem .5rem; + min-height: 36px; +} +.floating-install-btn { + bottom: 1rem; + left: 1rem; + right: 1rem; + width: calc(100vw - 2rem); +} +.install-selected-btn { + font-size: .9rem; + padding: .8rem 1rem; + min-height: 44px; +} +}.repo-manager-section { + margin-bottom: 2rem; +} +.repo-manager-toggle { + background: var(--vp-c-brand); + color: #fff; + border: 1px solid var(--vp-c-brand); + border-radius: 4px; + padding: .4rem .8rem; + cursor: pointer; + font-size: .8rem; + transition: all 0.2s ease; + min-height: 40px; +} +.repo-manager-toggle:hover { + background: var(--vp-c-brand-dark); + border-color: var(--vp-c-brand-dark); +} +.repo-manager-toggle:active { + transform: scale(.98); +} +.repo-manager { + background: #18181b; + border: 1px solid var(--vp-c-border); + border-radius: 8px; + padding: 1.5rem; + margin-top: 1rem; +} +.repo-manager h3 { + margin: 0 0 1rem 0; + color: #fff; + font-size: 1.1rem; +} +.add-repo-form { + display: flex; + gap: .5rem; + margin-bottom: 1rem; + flex-wrap: wrap; +} +.repo-input { + flex: 1; + min-width: 150px; + padding: .75rem; + border: 1px solid var(--vp-c-border); + border-radius: 4px; + background: #18181b; + color: #fff; + font-size: .9rem; +} +.repo-input:focus { + outline: none; + border-color: var(--vp-c-brand); +} +.add-repo-btn { + background: var(--vp-c-success); + color: #fff; + border: 1px solid var(--vp-c-success); + border-radius: 4px; + padding: .75rem 1rem; + cursor: pointer; + font-size: .9rem; + transition: all 0.2s ease; + min-height: 44px; +} +.add-repo-btn:hover { + background: var(--vp-c-success-dark); + border-color: var(--vp-c-success-dark); +} +.add-repo-btn:active { + transform: scale(.98); +} +.custom-repos-list { + display: flex; + flex-direction: column; + gap: .5rem; +} +.custom-repo-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem; + background: var(--vp-c-bg); + border: 1px solid var(--vp-c-border); + border-radius: 4px; +} +.repo-display-name { + color: var(--vp-c-text-1); + font-weight: 500; +} +.remove-repo-btn { + background: var(--vp-c-danger); + color: #fff; + border: 1px solid var(--vp-c-danger); + border-radius: 4px; + padding: .5rem 1rem; + cursor: pointer; + font-size: .8rem; + transition: all 0.2s ease; +} +.remove-repo-btn:hover { + background: var(--vp-c-danger-dark); + border-color: var(--vp-c-danger-dark); +} +.remove-repo-btn:active { + transform: scale(.98); +} +.no-custom-repos { + text-align: center; + color: var(--vp-c-text-2); + font-style: italic; + padding: 1rem; +} +.advanced-filters { + max-height: 0; + overflow: hidden; + transition: max-height 0.3s ease, opacity 0.3s ease, padding 0.3s ease; + opacity: 0; + padding: 0; + border-top: 0 solid var(--vp-c-border); +} +.advanced-filters.expanded { + max-height: 500px; + opacity: 1; + padding-top: 1rem; + border-top: 1px solid var(--vp-c-border); + margin-top: 1rem; +} +.filter-group { + margin-bottom: 1rem; +} +.filter-group:last-of-type { + margin-bottom: .5rem; +} +.filter-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: .5rem; +} +.filter-label { + font-size: .85rem; + font-weight: 600; + color: var(--vp-c-text-1); + text-transform: uppercase; + letter-spacing: .025em; +} +.clear-filter-btn { + background: none; + border: none; + color: var(--vp-c-text-2); + font-size: .75rem; + cursor: pointer; + padding: .2rem .5rem; + border-radius: 4px; + transition: all 0.2s ease; +} +.clear-filter-btn:hover { + background: var(--vp-c-bg-soft); + color: var(--vp-c-text-1); +} +.filter-chips { + display: flex; + flex-wrap: wrap; + gap: .4rem; +} +.filter-chip { + padding: .35rem .7rem; + background: var(--vp-c-bg-soft); + border: 1px solid #fff0; + border-radius: 20px; + color: var(--vp-c-text-2); + font-size: .8rem; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; +} +.filter-chip:hover { + background: var(--vp-c-bg); + border-color: var(--vp-c-border); + color: var(--vp-c-text-1); +} +.filter-chip.active { + background: var(--vp-c-brand); + color: #fff; + border-color: var(--vp-c-brand); +} +.filter-actions { + display: flex; + justify-content: flex-end; + padding-top: .75rem; + border-top: 1px solid var(--vp-c-border); + margin-top: .75rem; +} +.clear-all-btn { + background: none; + border: 1px solid var(--vp-c-border); + color: var(--vp-c-text-2); + font-size: .8rem; + padding: .5rem 1rem; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; +} +.clear-all-btn:hover { + background: var(--vp-c-danger-soft); + border-color: var(--vp-c-danger); + color: var(--vp-c-danger); +} +.filter-tag.language-tag { + background: var(--vp-c-indigo-soft); + color: var(--vp-c-indigo-1); +} +.filter-tag.genre-tag { + background: var(--vp-c-purple-soft); + color: var(--vp-c-purple-1); +} +.filter-tag.source-tag { + background: var(--vp-c-green-soft); + color: var(--vp-c-green-1); +} +.filter-tag.rating-tag { + background: var(--vp-c-yellow-soft); + color: var(--vp-c-yellow-1); +} +.filter-tag.rating-tag.rating-safe { + background: var(--vp-c-green-soft); + color: var(--vp-c-green-1); +} +.filter-tag.rating-tag.rating-mature { + background: var(--vp-c-yellow-soft); + color: var(--vp-c-yellow-1); +} +.filter-tag.rating-tag.rating-adult { + background: var(--vp-c-red-soft); + color: var(--vp-c-red-1); +} +.filter-chip.rating-safe.active { + background: var(--vp-c-green-soft); + color: var(--vp-c-green-1); + border-color: var(--vp-c-green-2); +} +.filter-chip.rating-mature.active { + background: var(--vp-c-yellow-soft); + color: var(--vp-c-yellow-1); + border-color: var(--vp-c-yellow-2); +} +.filter-chip.rating-adult.active { + background: var(--vp-c-red-soft); + color: var(--vp-c-red-1); + border-color: var(--vp-c-red-2); +} +.repos-section { + border-top-color: var(--vp-c-border); +} +.add-repo-inline { + display: flex; + gap: .5rem; +} +.repo-input-inline { + flex: 1; + padding: .5rem .75rem; + background: var(--vp-c-bg-soft); + border: 1px solid #fff0; + border-radius: 8px; + color: var(--vp-c-text-1); + font-size: .85rem; + transition: all 0.2s ease; +} +.repo-input-inline:focus { + outline: none; + border-color: var(--vp-c-brand); + background: var(--vp-c-bg); +} +.repo-input-inline::placeholder { + color: var(--vp-c-text-3); +} +.add-repo-btn-inline { + padding: .5rem 1rem; + background: var(--vp-c-brand); + border: none; + border-radius: 8px; + color: #fff; + font-size: .85rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; +} +.add-repo-btn-inline:hover { + background: var(--vp-c-brand-dark); +} +.add-repo-btn-inline:disabled { + opacity: .6; + cursor: not-allowed; +} +.repo-chip { + display: flex; + align-items: center; + gap: .4rem; + padding: .35rem .5rem .35rem .75rem; + background: var(--vp-c-bg-soft); + border: 1px solid var(--vp-c-border); + border-radius: 20px; + color: var(--vp-c-text-1); + font-size: .8rem; +} +.repo-chip-name { + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.repo-chip-remove { + display: flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + background: #fff0; + border: none; + border-radius: 50%; + color: var(--vp-c-text-2); + font-size: .7rem; + cursor: pointer; + transition: all 0.2s ease; +} +.repo-chip-remove:hover { + background: var(--vp-c-danger-soft); + color: var(--vp-c-danger); +} +.no-repos-hint { + padding: .75rem; + text-align: center; + color: var(--vp-c-text-3); + font-size: .85rem; + font-style: italic; +} +.extension-search-overlay { + position: fixed; + inset: 0; + background: rgb(0 0 0 / .6); + backdrop-filter: blur(4px); + z-index: 9999; + display: flex; + align-items: flex-start; + justify-content: center; + padding-top: 10vh; +} +.extension-search-modal { + width: 100%; + max-width: 600px; + margin: 0 1rem; + background: var(--vp-c-bg); + border: 1px solid var(--vp-c-border); + border-radius: 12px; + box-shadow: 0 25px 50px -12px rgb(0 0 0 / .5); + overflow: hidden; +} +.search-header { + padding: 1rem; + border-bottom: 1px solid var(--vp-c-border); +} +.search-input-wrapper { + display: flex; + align-items: center; + gap: .75rem; +} +.search-icon { + color: var(--vp-c-text-2); + flex-shrink: 0; +} +.search-input { + flex: 1; + background: #fff0; + border: none; + font-size: 1.1rem; + color: var(--vp-c-text-1); + outline: none; +} +.search-input::placeholder { + color: var(--vp-c-text-3); +} +.search-kbd { + padding: .2rem .5rem; + background: var(--vp-c-bg-soft); + border: 1px solid var(--vp-c-border); + border-radius: 4px; + font-size: .75rem; + color: var(--vp-c-text-2); + font-family: inherit; +} +.search-results { + max-height: 400px; + overflow-y: auto; +} +.search-loading, .search-empty, .search-hint { + padding: 2rem; + text-align: center; + color: var(--vp-c-text-2); +} +.search-loading { + display: flex; + align-items: center; + justify-content: center; + gap: .75rem; +} +.spinner { + width: 20px; + height: 20px; + border: 2px solid var(--vp-c-bg-soft); + border-top-color: var(--vp-c-brand); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} +@keyframes spin { + to { + transform: rotate(360deg); +} +}.results-list { + padding: .5rem; +} +.result-item { + display: flex; + align-items: center; + gap: .75rem; + width: 100%; + padding: .75rem; + background: #fff0; + border: none; + border-radius: 8px; + cursor: pointer; + text-align: left; + transition: background 0.15s; +} +.result-item:hover, .result-item.selected { + background: var(--vp-c-bg-soft); +} +.result-icon { + width: 40px; + height: 40px; + border-radius: 8px; + object-fit: cover; + flex-shrink: 0; +} +.result-info { + flex: 1; + min-width: 0; +} +.result-name { + font-weight: 500; + color: var(--vp-c-text-1); + display: flex; + align-items: center; + gap: .5rem; +} +.result-rating { + font-size: .7rem; + font-weight: 600; + text-transform: uppercase; + display: inline-block; + padding: .05rem .25rem; + border-radius: 6px; +} +.result-description { + font-size: .85rem; + color: var(--vp-c-text-2); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.result-source { + font-size: .75rem; + color: var(--vp-c-text-3); + text-transform: uppercase; + flex-shrink: 0; +} +.search-footer { + padding: .75rem 1rem; + border-top: 1px solid var(--vp-c-border); + background: var(--vp-c-bg-soft); +} +.footer-hint { + display: flex; + gap: .5rem; + align-items: center; + justify-content: center; + font-size: .8rem; + color: var(--vp-c-text-2); +} +.footer-hint kbd { + padding: .15rem .4rem; + background: var(--vp-c-bg); + border: 1px solid var(--vp-c-border); + border-radius: 4px; + font-size: .7rem; + font-family: inherit; +} +.modal-enter-active, .modal-leave-active { + transition: opacity 0.2s ease; +} +.modal-enter-active .extension-search-modal, .modal-leave-active .extension-search-modal { + transition: transform 0.2s ease, opacity 0.2s ease; +} +.modal-enter-from, .modal-leave-to { + opacity: 0; +} +.modal-enter-from .extension-search-modal, .modal-leave-to .extension-search-modal { + transform: scale(.95); + opacity: 0; +} +@media (max-width:768px) { + .extension-header { + flex-direction: row; + align-items: center; + text-align: left; + gap: .75rem; +} +.extension-header-text { + align-items: flex-start; +} +.extension-badges { + justify-content: flex-start; +} +} diff --git a/src/.vitepress/theme/components/ExtensionList.vue b/src/.vitepress/theme/components/ExtensionList.vue new file mode 100644 index 0000000..2547c7a --- /dev/null +++ b/src/.vitepress/theme/components/ExtensionList.vue @@ -0,0 +1,800 @@ + + + diff --git a/src/.vitepress/theme/components/ExtensionSearch.vue b/src/.vitepress/theme/components/ExtensionSearch.vue new file mode 100644 index 0000000..40dce22 --- /dev/null +++ b/src/.vitepress/theme/components/ExtensionSearch.vue @@ -0,0 +1,223 @@ + + + diff --git a/src/.vitepress/theme/index.ts b/src/.vitepress/theme/index.ts index c94b32f..86304e8 100644 --- a/src/.vitepress/theme/index.ts +++ b/src/.vitepress/theme/index.ts @@ -6,18 +6,24 @@ import type { Theme } from "vitepress"; import DefaultTheme from "vitepress/theme"; import { h } from "vue"; import "./style.css"; +import ExtensionList from "./components/ExtensionList.vue"; +import ExtensionSearch from "./components/ExtensionSearch.vue"; export default { extends: DefaultTheme, Layout: () => { return h(DefaultTheme.Layout, null, { // https://vitepress.dev/guide/extending-default-theme#layout-slots + "layout-bottom": () => + h("ClientOnly", null, { + default: () => h(ExtensionSearch), + }), }); }, enhanceApp({ app, router, siteData }) { void app; void router; void siteData; - // ... + app.component("ExtensionList", ExtensionList); }, } satisfies Theme; diff --git a/src/.vitepress/theme/lib/extensions.ts b/src/.vitepress/theme/lib/extensions.ts new file mode 100644 index 0000000..e5758ef --- /dev/null +++ b/src/.vitepress/theme/lib/extensions.ts @@ -0,0 +1,380 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +/* Copyright © 2025 Inkdex */ + +import { ref } from "vue"; + +export interface GitHubFile { + name: string; + path: string; + type: "file" | "dir"; + html_url: string; + download_url?: string; +} + +export interface ExtensionMetadata { + id: string; + name: string; + description: string; + version: string; + icon: string; + language: string | null; + contentRating: "SAFE" | "MATURE" | "ADULT"; + badges: Array<{ + label: string; + textColor: string; + backgroundColor: string; + }>; + capabilities: number | number[]; + developers: Array<{ + name: string; + website?: string | null; + github?: string | null; + }>; +} + +export interface Extension { + name: string; + source: string; + url: string; + html_url: string; + metadata?: ExtensionMetadata; + iconUrl?: string; + repoId?: string; +} + +export interface CustomRepository { + id: string; + name: string; + owner: string; + branch: string; + displayName: string; +} + +const STORAGE_KEY = "inkdex-custom-repos"; +const DEFAULT_REPOS: Omit[] = [ + { id: "inkdex", name: "extensions", owner: "inkdex", branch: "master" }, +]; + +export const getContentRatingColor = (rating: string): string => { + switch (rating) { + case "ADULT": + return "var(--vp-c-danger)"; + case "MATURE": + return "var(--vp-c-warning)"; + case "SAFE": + return "var(--vp-c-success)"; + default: + return "var(--vp-c-text-2)"; + } +}; + +export const getContentRatingBg = (rating: string): string => { + switch (rating) { + case "ADULT": + return "var(--vp-c-danger-soft)"; + case "MATURE": + return "var(--vp-c-warning-soft)"; + case "SAFE": + return "var(--vp-c-success-soft)"; + default: + return "var(--vp-c-bg-soft)"; + } +}; + +export const buildIconUrl = ( + repo: { owner: string; name: string; branch: string }, + extensionId: string, + iconPath: string, +): string => { + return `https://${repo.owner}.github.io/${repo.name}/0.9/stable/${extensionId}/static/${iconPath}`; +}; + +export const buildBaseUrl = (repo: { + owner: string; + name: string; + branch: string; +}): string => { + return `https://raw.githubusercontent.com/${repo.owner}/${repo.name}/${repo.branch}/0.9/stable`; +}; + +// Local Storage +export const loadCustomRepos = (): CustomRepository[] => { + if (typeof localStorage === "undefined") return []; + + const stored = localStorage.getItem(STORAGE_KEY); + if (stored) { + try { + return JSON.parse(stored); + } catch (e) { + console.error("Failed to parse custom repos from localStorage:", e); + return []; + } + } + return []; +}; + +export const saveCustomRepos = (repos: CustomRepository[]): void => { + if (typeof localStorage === "undefined") return; + localStorage.setItem(STORAGE_KEY, JSON.stringify(repos)); +}; + +export const fetchVersioningJson = async (repo: { + owner: string; + name: string; + branch: string; +}): Promise<{ sources: ExtensionMetadata[] } | null> => { + try { + const response = await fetch( + `https://raw.githubusercontent.com/${repo.owner}/${repo.name}/${repo.branch}/0.9/stable/versioning.json`, + ); + if (!response.ok) return null; + return await response.json(); + } catch (e) { + console.warn( + `Failed to fetch metadata from ${repo.owner}/${repo.name}:`, + e, + ); + return null; + } +}; + +export const fetchRepoContents = async (repo: { + owner: string; + name: string; + branch: string; +}): Promise => { + try { + const response = await fetch( + `https://api.github.com/repos/${repo.owner}/${repo.name}/contents/0.9/stable?ref=${repo.branch}`, + ); + if (!response.ok) return null; + return await response.json(); + } catch (e) { + console.warn(`Failed to fetch from ${repo.owner}/${repo.name}:`, e); + return null; + } +}; + +export const checkBranchExists = async ( + owner: string, + name: string, + branch: string, +): Promise => { + try { + const response = await fetch( + `https://api.github.com/repos/${owner}/${name}/branches/${branch}`, + ); + return response.ok; + } catch { + return false; + } +}; + +export const getDefaultBranch = async ( + owner: string, + name: string, +): Promise => { + try { + const response = await fetch( + `https://api.github.com/repos/${owner}/${name}`, + ); + if (response.ok) { + const data = await response.json(); + return data.default_branch; + } + } catch { + // Fall through to default + } + return "main"; +}; + +export const useExtensions = () => { + const extensions = ref([]); + const loading = ref(true); + const error = ref(null); + const customRepos = ref([]); + + const loadRepos = () => { + customRepos.value = loadCustomRepos(); + }; + + const getAllRepos = (): CustomRepository[] => { + const defaultWithDisplay = DEFAULT_REPOS.map((r) => ({ + ...r, + displayName: `${r.owner}/${r.name}`, + })); + return [...defaultWithDisplay, ...customRepos.value]; + }; + + const fetchAllExtensions = async (includeContents = true): Promise => { + loading.value = true; + error.value = null; + + try { + const allRepos = getAllRepos(); + + const metadataPromises = allRepos.map(async (repo) => { + const data = await fetchVersioningJson(repo); + return data ? { repo, data } : null; + }); + + const metadataResults = (await Promise.all(metadataPromises)).filter( + (r) => r !== null, + ); + + let contentsResults: Array<{ + repo: CustomRepository; + data: GitHubFile[]; + }> = []; + if (includeContents) { + const contentsPromises = allRepos.map(async (repo) => { + const data = await fetchRepoContents(repo); + return data ? { repo, data } : null; + }); + contentsResults = (await Promise.all(contentsPromises)).filter( + (r) => r !== null, + ) as Array<{ repo: CustomRepository; data: GitHubFile[] }>; + } + + const allExtensions: Extension[] = []; + + for (const metaResult of metadataResults) { + if (!metaResult) continue; + const { repo, data } = metaResult; + + const metadataMap = new Map( + data.sources.map((source: ExtensionMetadata) => [source.id, source]), + ); + + if (includeContents) { + const contentsResult = contentsResults.find( + (c) => c.repo.id === repo.id, + ); + if (contentsResult) { + const repoExtensions = contentsResult.data + .filter((item: GitHubFile) => item.type === "dir") + .map((item: GitHubFile) => { + const extensionMetadata = metadataMap.get(item.name) as + | ExtensionMetadata + | undefined; + return { + name: item.name, + source: repo.id, + url: `${buildBaseUrl(repo)}/${item.name}/index.js`, + html_url: item.html_url, + metadata: extensionMetadata, + repoId: repo.id, + iconUrl: extensionMetadata?.icon + ? buildIconUrl(repo, item.name, extensionMetadata.icon) + : "https://paperback.moe/pb-placeholder.png", + }; + }); + allExtensions.push(...repoExtensions); + } + } else { + for (const source of data.sources) { + allExtensions.push({ + name: source.id, + source: repo.id, + url: `${buildBaseUrl(repo)}/${source.id}/index.js`, + html_url: `https://github.com/${repo.owner}/${repo.name}/tree/${repo.branch}/0.9/stable/${source.id}`, + metadata: source, + repoId: repo.id, + iconUrl: source.icon + ? buildIconUrl(repo, source.id, source.icon) + : "https://paperback.moe/pb-placeholder.png", + }); + } + } + } + + extensions.value = allExtensions.sort((a, b) => + a.name.localeCompare(b.name), + ); + } catch (e) { + error.value = e instanceof Error ? e.message : "An error occurred"; + } finally { + loading.value = false; + } + }; + + const addCustomRepo = async ( + repoUrl: string, + ): Promise<{ success: boolean; error?: string }> => { + let owner = ""; + let name = ""; + let branch = ""; + + const url = repoUrl.trim(); + + if (url.includes("github.com/")) { + const match = url.match( + // eslint-disable-next-line + /github\.com\/([^\/]+)\/([^\/]+)(?:\/tree\/([^\/]+))?/, + ); + if (match) { + owner = match[1] || ""; + name = match[2] || ""; + if (match[3]) branch = match[3]; + } + } else if (url.includes("/")) { + const parts = url.split("/"); + if (parts.length >= 2) { + owner = parts[0] || ""; + name = parts[1] || ""; + } + } + + if (!owner || !name) { + return { + success: false, + error: + "Invalid repository URL. Please use format: owner/repo or https://github.com/owner/repo", + }; + } + + name = name.replace(/\.git$/, ""); + + // Auto-detect branch + if (!branch) { + const hasGhPages = await checkBranchExists(owner, name, "gh-pages"); + branch = hasGhPages ? "gh-pages" : await getDefaultBranch(owner, name); + } + + const id = `${owner}-${name}`; + const existing = customRepos.value.find((r) => r.id === id); + if (existing) { + return { success: false, error: "This repository is already added" }; + } + + const repo: CustomRepository = { + id, + name, + owner, + branch, + displayName: `${owner}/${name}`, + }; + + customRepos.value.push(repo); + saveCustomRepos(customRepos.value); + + return { success: true }; + }; + + const removeCustomRepo = (repoId: string): void => { + customRepos.value = customRepos.value.filter((r) => r.id !== repoId); + saveCustomRepos(customRepos.value); + }; + + return { + extensions, + loading, + error, + customRepos, + loadRepos, + getAllRepos, + fetchAllExtensions, + addCustomRepo, + removeCustomRepo, + }; +}; diff --git a/src/extension-list.md b/src/extension-list.md index d3dc441..2d67837 100644 --- a/src/extension-list.md +++ b/src/extension-list.md @@ -3,4 +3,6 @@ # Extensions -Coming soon! + + + From 0f8054247e76077dd6581069f3a28749d9918825 Mon Sep 17 00:00:00 2001 From: Eduard Smet Date: Mon, 15 Dec 2025 19:07:22 +0100 Subject: [PATCH 2/3] feat: Various extension list improvements --- .../theme/components/ExtensionDetails.vue | 273 ++-- .../theme/components/ExtensionList.css | 1227 +++++++++-------- .../theme/components/ExtensionList.vue | 788 +++++++---- .../theme/components/ExtensionSearch.vue | 4 +- src/.vitepress/theme/lib/extensions.ts | 227 ++- src/extension-list.md | 2 +- 6 files changed, 1537 insertions(+), 984 deletions(-) diff --git a/src/.vitepress/theme/components/ExtensionDetails.vue b/src/.vitepress/theme/components/ExtensionDetails.vue index e8fed64..1176baa 100644 --- a/src/.vitepress/theme/components/ExtensionDetails.vue +++ b/src/.vitepress/theme/components/ExtensionDetails.vue @@ -1,11 +1,20 @@