[feat] Extension details page (#2544)

* feat: add empty dialog

* feat: add `details_link` field for extension

* feat: show info icon if `details_link` present

* feat: add extension details endpoint

* feat: first details page

* feat: carousel working

* feat: full screen

* fix: layout

* fix: repo site

* fix: release icon

* fix: repo link

* feat: terms and conditions partial

* chore: fix typing

* fix: info icon layout

* chore: add try-catch

* feat: layout improvements

* feat: add video link

* fix: show terms and conditions

* chore: code format

* feat: add `details_link`

* fix: github release details

* feat: add close button

* chore: code clean-up

* chore: revert some changes

* feat: i18n

* chore: `make bundle`

* chore: make bundle

* feat: terms and conditions is a link now
This commit is contained in:
Vlad Stan 2024-06-19 13:52:18 +03:00 committed by GitHub
parent 76e8d72d0d
commit eacdd432b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 286 additions and 12 deletions

View File

@ -87,10 +87,17 @@
<div class="col-9 q-pl-sm">
<q-badge
v-if="hasNewVersion(extension)"
@click="showExtensionDetails(extension.id, extension.latestRelease?.details_link)"
color="green"
class="float-right"
:class="extension.latestRelease?.details_link ? 'cursor-pointer': ''"
>
<small v-text="$t('new_version')"></small>
<q-icon
v-if="extension.latestRelease?.details_link"
name="info"
size="xs"
></q-icon>
<small v-text="$t('new_version')" class="q-ma-xs"></small>
<q-tooltip
><span v-text="extension.latestRelease.version"></span
></q-tooltip>
@ -227,14 +234,27 @@
<div class="col-2">
<div
v-if="extension.isInstalled && extension.installedRelease"
v-if="(extension.isInstalled && extension.installedRelease) || extension.details_link"
class="float-right"
>
<q-badge>
<span v-text="extension.installedRelease.version"></span>
<q-tooltip>
<span v-text="$t('extension_installed_version')"></span>
</q-tooltip>
<q-badge
@click="showExtensionDetails(extension.id, extension.details_link)"
:class="extension.details_link? 'cursor-pointer' : ''"
>
<q-icon
v-if="extension.details_link"
name="info"
size="xs"
></q-icon>
<div v-if="extension.installedRelease" class="q-ma-xs">
<span
v-text="extension.installedRelease.version"
class="q-mt-lg"
></span>
<q-tooltip>
<span v-text="$t('extension_installed_version')"></span>
</q-tooltip>
</div>
</q-badge>
</div>
</div>
@ -754,23 +774,186 @@
</q-card-section>
</q-card>
</q-dialog>
<q-dialog v-model="showExtensionDetailsDialog">
<q-card
v-if="selectedExtensionDetails"
class="q-pa-lg"
style="width: 800px; max-width: 80vw"
>
<q-card-section>
<div class="row">
<div class="col-2 gt-md">
<q-img
:src="selectedExtensionDetails.icon"
style="width: 100px"
type="image"
></q-img>
</div>
<div class="col-7 q-pl-md">
<h3 class="q-my-sm" v-text="selectedExtensionDetails.name"></h3>
<h6
class="q-my-sm"
v-text="selectedExtensionDetails.short_description"
></h6>
</div>
<div class="col-3">
<q-btn
v-close-popup
flat
color="grey"
class="float-right q-ml-lg"
v-text="$t('close')"
></q-btn>
</div>
</div>
<div v-if="selectedExtensionDetails.images?.length" class="row q-my-lg">
<div class="col q-pr-md">
<q-carousel
swipeable
animated
v-model="slide"
:fullscreen.sync="fullscreen"
thumbnails
infinite
:autoplay="autoplay"
arrows
transition-prev="slide-right"
transition-next="slide-left"
@mouseenter="autoplay = false"
@mouseleave="autoplay = true"
height="300px"
>
<template v-slot:control>
<q-carousel-control position="bottom-right" :offset="[18, 18]">
<q-btn
push
round
dense
color="white"
text-color="primary"
:icon="fullscreen ? 'fullscreen_exit' : 'fullscreen'"
@click="fullscreen = !fullscreen"
></q-btn>
</q-carousel-control>
</template>
<q-carousel-slide
v-for="(image, i) of selectedExtensionDetails.images"
:img-src="image.uri"
:key="i"
:name="i"
>
<q-video
v-if="image.link"
class="absolute-full"
:src="image.link"
/>
</q-carousel-slide>
</q-carousel>
</div>
</div>
<div class="row">
<div class="col-8 q-pr-sm">
<div v-html="selectedExtensionDetails.description_md"></div>
</div>
<div class="col-4 q-pl-sm" style="border-left: 1px solid grey">
<div class="">
<q-btn
size="xs"
color="primary"
label="Terms and conditions"
type="a"
:href="selectedExtensionDetails.terms_and_conditions_md"
target="_blank"
rel="noopener noreferrer"
></q-btn>
</div>
<div class="q-mt-md">
<b>
<span v-text="$t('contributors')"></span>
</b>
<small>
<div
v-for="contributor of selectedExtensionDetails.contributors"
>
<a
:href="contributor.uri"
target="_blank"
rel="noopener noreferrer"
style="color: var(--q-primary); text-decoration: none"
>
<span
v-text="(contributor.name || contributor) + ' - ' + (contributor.role || 'dev')"
></span>
</a>
</div>
</small>
</div>
<div class="q-pt-lg">
<div>
<b>
<span v-text="$t('license')"></span>
</b>
<q-badge
color="primary"
v-text="selectedExtensionDetails.license"
></q-badge>
</div>
<br />
<div>
<q-rating
v-model="maxStars"
disable
size="1.5em"
:max="5"
color="primary"
><q-tooltip>
<span
v-text="$t('extension_rating_soon')"
></span> </q-tooltip
></q-rating>
<q-btn
size="xs"
color="primary"
:label="$t('repository')"
type="a"
:href="selectedExtensionDetails.repo"
target="_blank"
rel="noopener noreferrer"
></q-btn>
</div>
</div>
</div>
</div>
</q-card-section>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
new Vue({
el: '#vue',
data: function () {
return {
slide: 0,
fullscreen: false,
autoplay: true,
searchTerm: '',
tab: 'all',
manageExtensionTab: 'releases',
filteredExtensions: null,
showUninstallDialog: false,
showManageExtensionDialog: false,
showExtensionDetailsDialog: false,
showDropDbDialog: false,
showPayToEnableDialog: false,
dropDbExtensionId: '',
selectedExtension: null,
selectedImage: null,
selectedExtensionDetails: null,
selectedExtensionRepos: null,
selectedRelease: null,
uninstallAndDropDb: false,
@ -812,6 +995,11 @@
)
.filter(e => (tab === 'featured' ? e.isFeatured : true))
.filter(extensionNameContains(term))
.map(e => ({
...e,
details_link:
e.installedRelease?.details_link || e.latestRelease?.details_link
}))
this.tab = tab
},
@ -1069,6 +1257,29 @@
}
},
showExtensionDetails: async function (extId, detailsLink) {
if (!detailsLink) {
return
}
this.selectedExtensionDetails = null
this.showExtensionDetailsDialog = true
this.slide = 0
this.fullscreen = false
try {
const {data} = await LNbits.api.request(
'GET',
`/api/v1/extension/${extId}/details?details_link=${detailsLink}`,
this.g.user.wallets[0].inkey
)
this.selectedExtensionDetails = data
this.selectedExtensionDetails.description_md =
LNbits.utils.convertMarkdown(data.description_md)
} catch (error) {
console.warn(error)
}
},
async payAndInstall(release) {
try {
this.selectedExtension.inProgress = true

View File

@ -37,6 +37,7 @@ from lnbits.extension_manager import (
ReleasePaymentInfo,
UserExtensionInfo,
fetch_github_release_config,
fetch_release_details,
fetch_release_payment_info,
get_valid_extensions,
)
@ -128,6 +129,35 @@ async def api_install_extension(
) from exc
@extension_router.get("/{ext_id}/details", dependencies=[Depends(check_user_exists)])
async def api_extension_details(
ext_id: str,
details_link: str,
):
try:
all_releases = await InstallableExtension.get_extension_releases(ext_id)
release = next(
(r for r in all_releases if r.details_link == details_link), None
)
assert release, "Details not found for release"
release_details = await fetch_release_details(details_link)
assert release_details, "Cannot fetch details for release"
release_details["icon"] = release.icon
release_details["repo"] = release.repo
return release_details
except AssertionError as exc:
raise HTTPException(HTTPStatus.BAD_REQUEST, str(exc)) from exc
except Exception as exc:
logger.warning(exc)
raise HTTPException(
HTTPStatus.INTERNAL_SERVER_ERROR,
f"Failed to get details for extension {ext_id}.",
) from exc
@extension_router.put("/{ext_id}/sell")
async def api_update_pay_to_enable(
ext_id: str,

View File

@ -32,6 +32,7 @@ class ExplicitRelease(BaseModel):
warning: Optional[str]
info_notification: Optional[str]
critical_notification: Optional[str]
details_link: Optional[str]
pay_link: Optional[str]
def is_version_compatible(self):
@ -58,6 +59,9 @@ class GitHubRepoRelease(BaseModel):
zipball_url: str
html_url: str
def details_link(self, source_repo: str) -> str:
return f"https://raw.githubusercontent.com/{source_repo}/{self.tag_name}/config.json"
class GitHubRepo(BaseModel):
stargazers_count: str
@ -210,6 +214,24 @@ async def fetch_release_payment_info(
return None
async def fetch_release_details(details_link: str) -> Optional[dict]:
try:
async with httpx.AsyncClient() as client:
resp = await client.get(details_link)
resp.raise_for_status()
data = resp.json()
if "description_md" in data:
resp = await client.get(data["description_md"])
if not resp.is_error:
data["description_md"] = resp.text
return data
except Exception as e:
logger.warning(e)
return None
def icon_to_github_url(source_repo: str, path: Optional[str]) -> str:
if not path:
return ""
@ -315,6 +337,7 @@ class ExtensionRelease(BaseModel):
warning: Optional[str] = None
repo: Optional[str] = None
icon: Optional[str] = None
details_link: Optional[str] = None
pay_link: Optional[str] = None
cost_sats: Optional[int] = None
@ -347,6 +370,7 @@ class ExtensionRelease(BaseModel):
archive=r.zipball_url,
source_repo=source_repo,
is_github_release=True,
details_link=r.details_link(source_repo),
repo=f"https://github.com/{source_repo}",
html_url=r.html_url,
)
@ -366,6 +390,7 @@ class ExtensionRelease(BaseModel):
is_version_compatible=e.is_version_compatible(),
warning=e.warning,
html_url=e.html_url,
details_link=e.details_link,
pay_link=e.pay_link,
repo=e.repo,
icon=e.icon,
@ -613,18 +638,18 @@ class InstallableExtension(BaseModel):
repo, latest_release, config = await fetch_github_repo_info(
github_release.organisation, github_release.repository
)
source_repo = f"{github_release.organisation}/{github_release.repository}"
return InstallableExtension(
id=github_release.id,
name=config.name,
short_description=config.short_description,
stars=int(repo.stargazers_count),
icon=icon_to_github_url(
f"{github_release.organisation}/{github_release.repository}",
source_repo,
config.tile,
),
latest_release=ExtensionRelease.from_github_release(
repo.html_url, latest_release
source_repo, latest_release
),
)
except Exception as e:
@ -740,6 +765,12 @@ class CreateExtension(BaseModel):
payment_hash: Optional[str] = None
class ExtensionDetailsRequest(BaseModel):
ext_id: str
source_repo: str
version: str
def get_valid_extensions(include_deactivated: Optional[bool] = True) -> List[Extension]:
valid_extensions = [
extension for extension in ExtensionManager().extensions if extension.is_valid

File diff suppressed because one or more lines are too long

View File

@ -258,5 +258,7 @@ window.localisation.en = {
sell_info:
'The %{name} extension requires a payment of minimum %{amount} sats to enable.',
hide_empty_wallets: 'Hide empty wallets',
recheck: 'Recheck'
recheck: 'Recheck',
contributors: 'Contributors',
license: 'License'
}