diff --git a/src/.vitepress/theme/components/ExtensionDetails.vue b/src/.vitepress/theme/components/ExtensionDetails.vue new file mode 100644 index 0000000..1176baa --- /dev/null +++ b/src/.vitepress/theme/components/ExtensionDetails.vue @@ -0,0 +1,249 @@ + + + diff --git a/src/.vitepress/theme/components/ExtensionList.css b/src/.vitepress/theme/components/ExtensionList.css new file mode 100644 index 0000000..ee772e9 --- /dev/null +++ b/src/.vitepress/theme/components/ExtensionList.css @@ -0,0 +1,1607 @@ +/* Reusable Components */ +.btn-brand { + background: var(--vp-button-brand-bg); + color: var(--vp-button-brand-text); + border: none; + font-weight: 600; +} +.btn-brand:hover:not(:disabled) { + background: var(--vp-button-brand-hover-bg); +} +.btn-brand:active:not(:disabled) { + background: var(--vp-button-brand-active-bg); +} +.btn-brand:disabled { + opacity: .5; + cursor: not-allowed; +} + +/* Install Registry Button */ +.install-registry-btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 1rem 2rem; + border-radius: 50px; + font-size: 1.05rem; + text-decoration: none !important; + cursor: pointer; + transition: all 0.2s ease; + margin: 1.5rem auto; + box-shadow: 0 4px 12px rgb(0 0 0 / .2); + min-height: 4rem; + color: var(--vp-button-brand-text) !important; + background: var(--vp-button-brand-bg); + border: none; + font-weight: 600; +} +.install-registry-btn:hover { + box-shadow: 0 6px 16px rgb(0 0 0 / .3); + background: var(--vp-button-brand-hover-bg); + color: var(--vp-button-brand-text) !important; +} +.install-registry-btn:active { + transform: scale(.95); + box-shadow: 0 2px 8px rgb(0 0 0 / .2); + background: var(--vp-button-brand-active-bg); +} + +/* Input button shared styles */ +.input-btn { + position: absolute; + right: .5rem; + top: 50%; + transform: translateY(-50%); + display: flex; + align-items: center; + justify-content: center; + padding: .5rem .75rem; + line-height: 1; + font-size: .85rem; + border-radius: 8px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} +.input-btn:focus-visible { + outline: 2px solid var(--vp-c-brand); + outline-offset: 2px; +} +.input-btn.btn-secondary { + background: var(--vp-c-bg); + border: 1px solid var(--vp-c-border); +} +.input-btn.btn-secondary:hover { + background: var(--vp-c-bg-alt); + border-color: var(--vp-c-border); +} +.input-btn.btn-brand { + border: none; +} + +.btn-secondary { + padding: .75rem .85rem; + background: var(--vp-c-bg-soft); + border: 1px solid var(--vp-c-divider); + color: var(--vp-c-text-2); + font-size: .85rem; + cursor: pointer; + border-radius: 8px; + transition: all 0.2s ease; + font-weight: 500; + line-height: 1; +} +.btn-secondary:hover { + background: var(--vp-c-bg); + border-color: var(--vp-c-border); + color: var(--vp-c-text-1); +} +.btn-secondary:focus-visible { + outline: 2px solid var(--vp-c-brand); + outline-offset: 2px; +} + +.badge { + display: inline-flex; + align-items: center; + justify-content: center; + padding: .25rem .5rem; + border-radius: 8px; + font-size: .7rem; + font-weight: 600; + gap: .25rem; +} + +.section-heading { + margin: 0 0 .75rem 0; + font-size: .85rem; + font-weight: 600; + color: var(--vp-c-text-1); +} + +.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: 1.5rem; +} +.search-bar { + width: 100%} +.extension-list .search-input-container { + position: relative; + display: flex; + align-items: center; +} +.extension-list .search-icon { + position: absolute; + left: 1rem; + top: 50%; + transform: translateY(-50%); + color: var(--vp-c-text-3); + pointer-events: none; + z-index: 1; +} +.extension-list .search-input { + width: 100%; + padding: .75rem 1rem .75rem 2.5rem; + background: var(--vp-c-bg-soft); + border: 1px solid var(--vp-c-divider); + border-radius: 8px; + color: var(--vp-c-text-1); + font-size: .95rem; + transition: all 0.2s ease; +} +.extension-list .search-input:focus { + outline: none; + background: var(--vp-c-bg-soft); + border-color: var(--vp-c-brand); + box-shadow: 0 0 0 2px var(--vp-c-brand-soft); +} +.extension-list .search-input::-webkit-search-cancel-button { + display: none; +} + + +.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 buttons (Filters, Repositories) */ +.action-btn { + position: relative; + padding: 0 1.75rem 0 .85rem; + background: var(--vp-c-bg-soft); + border: 1px solid var(--vp-c-divider); + border-radius: 8px; + color: var(--vp-c-text-2); + font-size: .85rem; + font-weight: 500; + cursor: pointer; + transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease; + line-height: 1; + white-space: nowrap; + display: inline-flex; + align-items: center; + gap: .5rem; + height: 2.5rem; +} +.action-btn:hover { + background: var(--vp-c-bg); + color: var(--vp-c-text-1); + border-color: var(--vp-c-border); +} +.action-btn.active { + background: var(--vp-button-brand-bg); + color: var(--vp-button-brand-text); + border-color: var(--vp-button-brand-bg); +} +.action-btn .btn-text { + line-height: 1; +} +.action-btn .filter-count { + background: rgb(255 255 255 / .25); + padding: .2rem .4rem; + border-radius: 10px; + font-size: .75rem; + min-width: 1.2rem; + text-align: center; + line-height: 1; + flex-shrink: 0; + visibility: hidden; + width: 0; + min-width: 0; + padding: 0; + margin: 0; +} +.action-btn .filter-count:not(:empty) { + visibility: visible; + width: auto; + min-width: 1.2rem; + padding: .2rem .4rem; +} +.action-btn .expand-icon { + position: absolute; + right: .5rem; + top: 50%; + transform: translateY(-50%); + font-size: .65rem; + line-height: 1; + transition: transform 0.2s ease; +} +.action-btn .expand-icon.expanded { + transform: translateY(-50%) 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; + flex-direction: column; + align-items: flex-start; + gap: .5rem; + 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); +} +.hidden-count { + color: var(--vp-c-text-3); + font-style: italic; + white-space: nowrap; +} +@media (max-width: 768px) { + .hidden-count { + display: block; + margin-top: .25rem; + white-space: normal; + } +} +.active-filters { + display: flex; + gap: .5rem; + overflow-x: auto; + overflow-y: hidden; + scrollbar-width: thin; + max-width: 100%; +} +.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; + white-space: nowrap; + flex-shrink: 0; +} +.filter-tag.nsfw-active { + background: var(--vp-c-danger-soft); + color: var(--vp-c-danger); +} +.filter-tag.negated-tag { + text-decoration: line-through; + opacity: 0.7; +} +.extensions-grid { + display: grid; + grid-template-columns: 1fr; + gap: 1.5rem; + margin: 1.5rem 0 1rem 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-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: 72px; + height: 72px; + 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: .5rem; + 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-repo { + display: flex; +} +.extension-source { + margin: 0; +} +.source-badge { + padding: .25rem .5rem; + border-radius: 8px; + font-size: .7rem; + font-weight: 500; + background: var(--vp-c-brand-soft); + color: var(--vp-c-brand); + white-space: nowrap; + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; +} +.extension-meta { + margin-bottom: 1rem; + transition: opacity 0.6s cubic-bezier(.4, 0, .2, 1); + display: flex; + flex-direction: column; + gap: .5rem; +} +.extension-description { + 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; +} +.extension-meta .extension-repo { + order: 10; + margin-top: auto; + padding-top: .25rem; +} +.content-rating { + margin-bottom: .5rem; +} +.rating-badge { + display: inline-flex; + align-items: center; + justify-content: center; + padding: .25rem .5rem; + border-radius: 8px; + font-size: .7rem; + font-weight: 600; +} +.header-badges { + display: flex; + gap: .5rem; + margin-top: .25rem; + align-items: center; +} +.version-badge { + display: inline-flex; + align-items: center; + justify-content: center; + padding: .25rem .5rem; + border-radius: 8px; + font-size: .7rem; + font-weight: 500; + background: var(--vp-c-bg-soft); + color: var(--vp-c-text-2); + border: 1px solid var(--vp-c-divider); +} +.extension-badges { + display: flex; + gap: .25rem; + overflow-x: auto; + overflow-y: hidden; + scrollbar-width: thin; +} +.genre-badge { + padding: .25rem .5rem; + border-radius: 8px; + font-size: .7rem; + font-weight: 500; + white-space: nowrap; + flex-shrink: 0; +} +.extension-tags-row { + display: flex; + gap: .5rem; + overflow-x: auto; + overflow-y: hidden; + scrollbar-width: thin; +} +.extension-services { + display: flex; + gap: .25rem; + overflow-x: auto; + overflow-y: hidden; + scrollbar-width: thin; +} +.service-badge { + padding: .25rem .5rem; + border-radius: 8px; + font-size: .7rem; + font-weight: 500; + white-space: nowrap; + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; +} +.service-badge.service-content { + background: #3b82f626; + color: #3b82f6; +} +.service-badge.service-tracker { + background: #10b98126; + color: #10b981; +} +.service-badge.service-cloudflare { + background: #ef444426; + color: #ef4444; +} +.extension-language { +} +.language-badge { + padding: .25rem .5rem; + border-radius: 8px; + font-size: .7rem; + font-weight: 500; + background: var(--vp-c-bg-soft); + color: var(--vp-c-text-2); + white-space: nowrap; + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; +} +.card-badges { + display: flex; + gap: .5rem; + align-items: center; +} +.extension-footer { + margin-top: auto; + display: flex; + align-items: center; + justify-content: space-between; + gap: .75rem; +} +.extension-actions { + display: flex; + gap: .75rem; + align-items: center; + margin-left: auto; + transition: opacity 0.6s cubic-bezier(.4, 0, .2, 1); +} +.details-btn { + padding: .75rem 1rem; + text-decoration: none; + font-size: .9rem; + font-weight: 500; + transition: all 0.2s ease; + display: inline-block; + text-align: center; + cursor: pointer; + white-space: nowrap; +} +.selected-indicator { + color: var(--vp-c-brand); + font-weight: 600; + font-size: .85rem; + white-space: nowrap; + flex-shrink: 0; +} +.floating-install-btn { + position: fixed; + bottom: 2rem; + right: 2rem; + z-index: 100; +} +.install-selected-btn { + display: flex; + align-items: center; + gap: .75rem; + padding: .7rem 1rem; + border-radius: 50px; + font-size: .9rem; + font-weight: 600; + box-shadow: 0 4px 12px rgb(0 0 0 / .2); + height: 3rem; + min-width: 4rem; + justify-content: center; +} +.install-selected-btn:hover:not(:disabled) { + box-shadow: 0 6px 16px rgb(0 0 0 / .3); +} +.install-selected-btn:active:not(:disabled) { + 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 { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.75rem; + text-align: center; + padding: 3rem 1rem; + color: var(--vp-c-text-2); +} +.no-results-icon { + font-size: 3rem; + line-height: 1; +} +.no-results h3 { + margin: 0; + color: var(--vp-c-text-1); +} +.no-results p { + margin: 0; +} +.clear-search-btn { + padding: .75rem 1rem; + border-radius: 8px; + font-size: .9rem; + min-height: 44px; + background: var(--vp-c-bg-soft); + border: 1px solid var(--vp-c-divider); + color: var(--vp-c-text-2); +} +.clear-search-btn:hover { + background: var(--vp-c-bg); + color: var(--vp-c-text-1); + border-color: var(--vp-c-border); +} +.clear-search-btn:focus-visible { + outline: 2px solid var(--vp-c-brand); + outline-offset: 2px; +} +.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 (min-width:720px) { + .extensions-grid { + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + } +}@media (min-width:500px) { +.install-selected-btn { + padding: 1rem 1.5rem; + font-size: .95rem; + height: 3.5rem; +} +}@media (min-width:700px) { +.install-selected-btn { + padding: 0 2rem; + font-size: 1.05rem; + height: 4rem; +} +}@media (max-width:480px) { +.extension-card { + padding: .75rem; +} +.extension-icon { + width: 48px; + height: 48px; +} +.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 / .6); + backdrop-filter: blur(4px); + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem; +} +.details-modal { + position: relative; + background: var(--vp-c-bg); + border-radius: 12px; + box-shadow: 0 25px 50px -12px rgb(0 0 0 / .5); + width: 100%; + max-width: 600px; + max-height: calc(100vh - 4rem); + overflow-y: auto; + border: 1px solid var(--vp-c-border); +} +.details-close-btn { + position: absolute; + top: 1rem; + right: 1rem; + background: var(--vp-c-bg-soft); + border: 1px solid var(--vp-c-border); + color: var(--vp-c-text-2); + cursor: pointer; + padding: .5rem; + border-radius: 6px; + transition: all 0.2s ease; + z-index: 10; + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; +} +.details-close-btn:hover { + background: var(--vp-c-bg); + color: var(--vp-c-text-1); + border-color: var(--vp-c-text-2); +} +.details-close-btn:active { + transform: scale(.95); +} +.details-content { + padding: 1.5rem; +} +.details-header { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 1rem; + padding-right: 2.5rem; + padding-bottom: 1rem; + border-bottom: 1px solid var(--vp-c-divider); +} +.details-icon { + width: 80px; + height: 80px; + border-radius: 12px; + object-fit: cover; + flex-shrink: 0; + box-shadow: 0 2px 8px rgb(0 0 0 / .1); +} +.details-header-text { + flex: 1; + display: flex; + flex-direction: column; + gap: .5rem; + min-width: 0; +} +.details-title { + margin: 0; + font-size: 1.5rem; + font-weight: 600; + color: var(--vp-c-text-1); + line-height: 1.2; + border: none; + padding: 0; +} +.details-content h2 { + margin: 0; + border: none; + padding: 0; +} +.details-header-badges { + display: flex; + gap: .5rem; + align-items: center; + flex-wrap: wrap; +} + +.details-section { + margin-bottom: 1rem; +} +.details-section:last-of-type { + margin-bottom: 0; +} +.details-section h3 { + margin: 0 0 .5rem 0; + font-size: .9rem; + font-weight: 600; + color: var(--vp-c-text-1); +} +.details-description { + margin-bottom: 1rem; +} +.details-description h3 { + margin: 0 0 .5rem 0; + font-size: .9rem; + font-weight: 600; + color: var(--vp-c-text-1); +} +.details-description p { + margin: 0; + line-height: 1.6; + color: var(--vp-c-text-2); +} +.details-rating-badge { + padding: .25rem .5rem; + border-radius: 8px; + font-size: .7rem; + font-weight: 600; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.details-version-badge { + padding: .25rem .5rem; + border-radius: 8px; + font-size: .7rem; + font-weight: 500; + background: var(--vp-c-bg-soft); + color: var(--vp-c-text-2); + border: 1px solid var(--vp-c-divider); + display: inline-flex; + align-items: center; + justify-content: center; +} +.details-services { + display: flex; + gap: .5rem; + overflow-x: auto; + overflow-y: hidden; + scrollbar-width: thin; +} + +.details-badges { + display: flex; + gap: .5rem; + overflow-x: auto; + overflow-y: hidden; + scrollbar-width: thin; +} +.details-developers { + display: flex; + flex-direction: column; + gap: .5rem; + padding-left: 1rem; +} +.details-developer { + display: flex; + flex-direction: column; + gap: .5rem; +} +.dev-name { + font-weight: 500; + color: var(--vp-c-text-1); + font-size: .9rem; +} +.dev-links { + display: flex; + gap: .5rem; + flex-wrap: wrap; +} +.dev-link { + font-size: .85rem; + color: var(--vp-c-brand); + text-decoration: none; + padding: .4rem .75rem; + border-radius: 8px; + background: transparent; + transition: all 0.2s ease; + border: 1px solid var(--vp-c-divider); + display: inline-flex; + align-items: center; + gap: .5rem; + width: fit-content; +} +.dev-link:hover { + background: var(--vp-c-bg-soft); + border-color: var(--vp-c-brand); +} +.dev-link svg { + width: 14px; + height: 14px; +} +.details-description { + margin-bottom: 1rem; +} +.details-description h3 { + margin: 0 0 .5rem 0; + font-size: .9rem; + font-weight: 600; + color: var(--vp-c-text-1); +} +.details-description p { + margin: 0; + color: var(--vp-c-text-2); + line-height: 1.6; + font-size: .95rem; +} +.details-actions { + display: flex; + justify-content: center; + margin-top: 1.5rem; +} +.details-install-btn { + padding: .85rem 2rem; + border-radius: 8px; + font-size: .95rem; + min-height: 44px; + width: 100%; +} +.details-install-btn:hover:not(:disabled) { + background: var(--vp-button-brand-hover-bg); + box-shadow: 0 4px 12px rgb(0 0 0 / .2); +} +.details-install-btn:active:not(:disabled) { + background: var(--vp-button-brand-active-bg); + transform: translateY(0); + box-shadow: 0 2px 6px rgb(0 0 0 / .2); +} +@media (max-width:600px) { + .details-overlay { + padding: 1rem; + } + .details-modal { + max-height: calc(100vh - 2rem); + } + .details-content { + padding: 1.5rem; + } + .details-header { + padding-right: 0; + margin-bottom: 1.5rem; + } + .details-icon { + width: 64px; + height: 64px; + } + .details-title { + font-size: 1.4rem; + } + .details-close-btn { + top: .75rem; + right: .75rem; + } +}.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 !important; + 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); +} + +.custom-repos-list { + display: flex; + gap: .5rem; + overflow-x: auto; + overflow-y: hidden; + white-space: nowrap; + scrollbar-width: thin; +} +.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; + flex-shrink: 0; + min-width: 250px; +} +.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 { + position: relative; + max-height: 0; + overflow: hidden; + transition: max-height 0.3s ease, opacity 0.3s ease, margin-top 0.3s ease, padding-top 0.3s ease, border-top-width 0.3s ease; + opacity: 0; + margin-top: -1rem; + padding-top: 0; + border-top: 0 solid var(--vp-c-border); +} +.advanced-filters.expanded { + max-height: 500px; + opacity: 1; + margin-top: 0; + padding-top: 1rem; + border-top: 1px solid var(--vp-c-border); +} +.filters-header { + position: absolute; + top: 1rem; + right: 0; + z-index: 10; +} +.filter-group { + margin-bottom: 1rem; +} +.filter-group:last-of-type { + margin-bottom: .5rem; +} +.repos-filter-group { + position: relative; +} + +.filter-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: .5rem; +} +.repos-filter-header { + margin-bottom: 0.5rem; +} +.repos-clear-btn { + position: absolute; + top: 0; + right: 0; + z-index: 1; +} +.filter-label { + font-size: .95rem; + font-weight: 600; + color: var(--vp-c-text-1); +} +.badge-mode-toggle { + padding: .2rem .5rem; + background: var(--vp-c-bg-soft); + border: 1px solid var(--vp-c-divider); + border-radius: 4px; + color: var(--vp-c-text-2); + font-size: .7rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + min-width: 42px; + font-family: monospace; + letter-spacing: 0.5px; +} +.badge-mode-toggle:hover { + background: var(--vp-c-bg); + color: var(--vp-c-text-1); + border-color: var(--vp-c-brand); +} +.clear-filter-btn { + padding: .2rem .5rem; + font-size: .75rem; +} +.filter-chips { + display: flex; + gap: .4rem; + overflow-x: auto; + overflow-y: hidden; + white-space: nowrap; + scrollbar-width: thin; +} +.filter-chip { + padding: .35rem .7rem; + background: var(--vp-c-bg-soft); + border: 1px solid var(--vp-c-divider); + border-radius: 20px; + color: var(--vp-c-text-2); + font-size: .8rem; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; + flex-shrink: 0; +} +.filter-chip:hover { + background: var(--vp-c-bg); + color: var(--vp-c-text-1); + border-color: var(--vp-c-border); +} +.filter-chip:focus-visible { + outline: 2px solid var(--vp-c-brand); + outline-offset: 2px; +} +.filter-chip.active { + background: var(--vp-c-brand); + color: #fff; + border-color: var(--vp-c-brand); +} +.filter-chip.negated { + background: #18181b; + color: #71717a; + border-color: #27272a; + text-decoration: line-through; +} +.filter-chip.service-content.active { + background: #3b82f6; + border-color: #3b82f6; +} +.filter-chip.service-tracker.active { + background: #10b981; + border-color: #10b981; +} +.filter-chip.service-cloudflare.active { + background: #ef4444; + border-color: #ef4444; +} +.filter-chip.filter-language.active { + background: #0891b2; + border-color: #0891b2; +} +.filter-chip.filter-badge.active { + background: #8b5cf6; + border-color: #8b5cf6; +} +.filter-actions { + display: flex; + justify-content: flex-end; + padding-top: .75rem; + border-top: 1px solid var(--vp-c-divider); + margin-top: .75rem; +} + +.filter-tag.language-tag { + background: #0891b226; + color: #0891b2; +} +.filter-tag.label-tag { + background: #8b5cf626; + color: #8b5cf6; +} +.filter-tag.repository-tag { + background: var(--vp-c-bg-soft); + color: var(--vp-c-text-2); +} +.filter-tag.service-tag.service-content { + background: #3b82f626; + color: #3b82f6; +} +.filter-tag.service-tag.service-tracker { + background: #10b98126; + color: #10b981; +} +.filter-tag.service-tag.service-cloudflare { + background: #ef444426; + color: #ef4444; +} +.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); +} +.extension-list .repo-input-container { + margin-bottom: 0; +} +.extension-list .repo-input { + padding-right: 5rem; +} + +.repo-chip { + display: flex; + align-items: center; + background: var(--vp-c-bg-soft); + border: 1px solid var(--vp-c-divider); + border-radius: 20px; + font-size: .8rem; + transition: all 0.2s ease; +} +.repo-chip.active { + background: var(--vp-c-brand); + border-color: var(--vp-c-brand); +} +.repo-chip.negated { + background: #18181b; + border-color: #27272a; +} +.repo-chip.negated .repo-chip-name { + color: #71717a; + text-decoration: line-through; +} +.repo-chip-name { + padding: .35rem .7rem; + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--vp-c-text-2); + border: none; + background: transparent; + cursor: default; + flex: 1; +} +.repo-chip-name.clickable { + cursor: pointer; + transition: color 0.2s ease; +} +.repo-chip-name.clickable:hover { + color: var(--vp-c-text-1); +} +.repo-chip.active .repo-chip-name { + color: #fff; +} +.repo-chip-remove { + display: flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + background: transparent; + border: none; + border-radius: 50%; + color: var(--vp-c-text-3); + font-size: .7rem; + cursor: pointer; + transition: color 0.2s ease; + flex-shrink: 0; + padding-right: .5rem; +} +.repo-chip-remove:hover { + color: var(--vp-c-text-1); +} +.repo-chip.active .repo-chip-remove { + color: rgba(255, 255, 255, 0.7); +} +.repo-chip.active .repo-chip-remove:hover { + color: #fff; +} +.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; + width: 14px; + height: 14px; +} +.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..cdd3b49 --- /dev/null +++ b/src/.vitepress/theme/components/ExtensionList.vue @@ -0,0 +1,1076 @@ + + + diff --git a/src/.vitepress/theme/components/ExtensionSearch.vue b/src/.vitepress/theme/components/ExtensionSearch.vue new file mode 100644 index 0000000..c3e67f7 --- /dev/null +++ b/src/.vitepress/theme/components/ExtensionSearch.vue @@ -0,0 +1,221 @@ + + + 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..d494e97 --- /dev/null +++ b/src/.vitepress/theme/lib/extensions.ts @@ -0,0 +1,597 @@ +/* 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)"; + } +}; + +// Map common language strings to valid IETF language tags +export const normalizeLanguageTag = (language: string | null): string => { + if (!language) return "en"; + + const langMap: Record = { + english: "en", + en: "en", + spanish: "es", + es: "es", + french: "fr", + fr: "fr", + german: "de", + de: "de", + italian: "it", + it: "it", + portuguese: "pt", + pt: "pt", + russian: "ru", + ru: "ru", + japanese: "ja", + ja: "ja", + chinese: "zh", + zh: "zh", + korean: "ko", + ko: "ko", + arabic: "ar", + ar: "ar", + turkish: "tr", + tr: "tr", + polish: "pl", + pl: "pl", + dutch: "nl", + nl: "nl", + indonesian: "id", + id: "id", + thai: "th", + th: "th", + vietnamese: "vi", + vi: "vi", + hindi: "hi", + hi: "hi", + }; + + const normalized = language.toLowerCase().trim(); + return langMap[normalized] || normalized; +}; + +// Get full language name from language code +export const getLanguageName = (language: string | null): string => { + if (!language) return "English"; + + const normalized = normalizeLanguageTag(language); + + if (normalized.toLowerCase() === "multi") { + return "Multi Language"; + } + + const languageNames: Record = { + en: "English", + es: "Spanish", + fr: "French", + de: "German", + it: "Italian", + pt: "Portuguese", + ru: "Russian", + ja: "Japanese", + zh: "Chinese", + ko: "Korean", + ar: "Arabic", + tr: "Turkish", + pl: "Polish", + nl: "Dutch", + id: "Indonesian", + th: "Thai", + vi: "Vietnamese", + hi: "Hindi", + }; + + return ( + languageNames[normalized] || + normalized.charAt(0).toUpperCase() + normalized.slice(1) + ); +}; + +// Get flag emoji for language code +export const getLanguageEmoji = (language: string | null): string | null => { + if (!language) return null; + + const normalized = normalizeLanguageTag(language); + + if (normalized.toLowerCase() === "multi") { + return "🌐"; + } + + const emojiMap: Record = { + en: "🇬🇧", + es: "🇪🇸", + fr: "🇫🇷", + de: "🇩🇪", + it: "🇮🇹", + pt: "🇵🇹", + ru: "🇷🇺", + ja: "🇯🇵", + zh: "🇨🇳", + ko: "🇰🇷", + ar: "🇸🇦", + tr: "🇹🇷", + pl: "🇵🇱", + nl: "🇳🇱", + id: "🇮🇩", + th: "🇹🇭", + vi: "🇻🇳", + hi: "🇮🇳", + }; + + return emojiMap[normalized] || null; +}; + +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`; +}; + +// Cache for API responses (1 minute TTL) +const CACHE_TTL = 60 * 1000; // 1 minute in milliseconds +const CACHE_STORAGE_KEY = "inkdex_api_cache"; + +interface CacheEntry { + data: any; + timestamp: number; +} + +interface CacheStorage { + [key: string]: CacheEntry; +} + +const loadCache = (): CacheStorage => { + if (typeof localStorage === "undefined") return {}; + try { + const stored = localStorage.getItem(CACHE_STORAGE_KEY); + return stored ? JSON.parse(stored) : {}; + } catch { + return {}; + } +}; + +const saveCache = (cache: CacheStorage): void => { + if (typeof localStorage === "undefined") return; + try { + localStorage.setItem(CACHE_STORAGE_KEY, JSON.stringify(cache)); + } catch (e) { + console.warn("Failed to save cache to localStorage:", e); + } +}; + +const getCachedData = (key: string): any | null => { + const cache = loadCache(); + const cached = cache[key]; + if (!cached) return null; + + const isExpired = Date.now() - cached.timestamp > CACHE_TTL; + if (isExpired) { + delete cache[key]; + saveCache(cache); + return null; + } + + return cached.data; +}; + +const setCachedData = (key: string, data: any): void => { + const cache = loadCache(); + cache[key] = { data, timestamp: Date.now() }; + saveCache(cache); +}; + +// Capability flags (SourceIntents) +const CAPABILITY_CHAPTER_PROVIDING = 1 << 0; // 1 +const CAPABILITY_MANGA_PROGRESS_PROVIDING = 1 << 1; // 2 +const CAPABILITY_CLOUDFLARE_BYPASS_PROVIDING = 1 << 4; // 16 + +export const hasCapability = ( + capabilities: number | number[], + flag: number, +): boolean => { + if (Array.isArray(capabilities)) { + return capabilities.includes(flag); + } + return (capabilities & flag) === flag; +}; + +export const hasChapterProviding = (capabilities: number | number[]): boolean => + hasCapability(capabilities, CAPABILITY_CHAPTER_PROVIDING); + +export const hasMangaProgressProviding = ( + capabilities: number | number[], +): boolean => hasCapability(capabilities, CAPABILITY_MANGA_PROGRESS_PROVIDING); + +export const hasCloudflareBypassProviding = ( + capabilities: number | number[], +): boolean => + hasCapability(capabilities, CAPABILITY_CLOUDFLARE_BYPASS_PROVIDING); + +// 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> => { + const cacheKey = `versioning:${repo.owner}/${repo.name}/${repo.branch}`; + const cached = getCachedData(cacheKey); + if (cached) return cached; + + 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; + const data = await response.json(); + setCachedData(cacheKey, data); + return data; + } 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 => { + const cacheKey = `contents:${repo.owner}/${repo.name}/${repo.branch}`; + const cached = getCachedData(cacheKey); + if (cached) return cached; + + 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; + const data = await response.json(); + setCachedData(cacheKey, data); + return data; + } 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, + showLoading = true, + ): Promise => { + if (showLoading) { + 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 { + if (showLoading) { + 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..651fb5a 100644 --- a/src/extension-list.md +++ b/src/extension-list.md @@ -1,6 +1,10 @@ -# Extensions +# Extension List -Coming soon! +Search and filter through all available extensions by language, content rating, badges, and services to find exactly what you need. Install extensions individually or add custom repositories to filter through as well. + + + + diff --git a/src/installation.md b/src/installation.md index 761b6bd..b229006 100644 --- a/src/installation.md +++ b/src/installation.md @@ -19,9 +19,11 @@ Extensions listed here are compatible with Paperback version 0.9. If you're usin The extension registry contains all extensions contributed by the Inkdex community. -Click the installation link below to add it to your Paperback app. +Click the button below to add it to your Paperback app. -# [**Install**]() + ::: details Install Manually @@ -32,6 +34,4 @@ Click the installation link below to add it to your Paperback app. The registry is hosted on GitHub and can be found [here](https://github.com/inkdex/extensions). It is completely automated, for more info you can join our [Discord server](https://discord.gg/inkdex) and ask us about it! -## Extension List - - +For a more detailed browsing experience, visit our [extension list page](/extension-list) where you can search, filter, and install extensions individually.