[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:
parent
76e8d72d0d
commit
eacdd432b2
5 changed files with 286 additions and 12 deletions
|
|
@ -87,10 +87,17 @@
|
||||||
<div class="col-9 q-pl-sm">
|
<div class="col-9 q-pl-sm">
|
||||||
<q-badge
|
<q-badge
|
||||||
v-if="hasNewVersion(extension)"
|
v-if="hasNewVersion(extension)"
|
||||||
|
@click="showExtensionDetails(extension.id, extension.latestRelease?.details_link)"
|
||||||
color="green"
|
color="green"
|
||||||
class="float-right"
|
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
|
<q-tooltip
|
||||||
><span v-text="extension.latestRelease.version"></span
|
><span v-text="extension.latestRelease.version"></span
|
||||||
></q-tooltip>
|
></q-tooltip>
|
||||||
|
|
@ -227,14 +234,27 @@
|
||||||
|
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
<div
|
<div
|
||||||
v-if="extension.isInstalled && extension.installedRelease"
|
v-if="(extension.isInstalled && extension.installedRelease) || extension.details_link"
|
||||||
class="float-right"
|
class="float-right"
|
||||||
>
|
>
|
||||||
<q-badge>
|
<q-badge
|
||||||
<span v-text="extension.installedRelease.version"></span>
|
@click="showExtensionDetails(extension.id, extension.details_link)"
|
||||||
<q-tooltip>
|
:class="extension.details_link? 'cursor-pointer' : ''"
|
||||||
<span v-text="$t('extension_installed_version')"></span>
|
>
|
||||||
</q-tooltip>
|
<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>
|
</q-badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -754,23 +774,186 @@
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</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>
|
</div>
|
||||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||||
<script>
|
<script>
|
||||||
new Vue({
|
new Vue({
|
||||||
el: '#vue',
|
el: '#vue',
|
||||||
|
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
|
slide: 0,
|
||||||
|
fullscreen: false,
|
||||||
|
autoplay: true,
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
tab: 'all',
|
tab: 'all',
|
||||||
manageExtensionTab: 'releases',
|
manageExtensionTab: 'releases',
|
||||||
filteredExtensions: null,
|
filteredExtensions: null,
|
||||||
showUninstallDialog: false,
|
showUninstallDialog: false,
|
||||||
showManageExtensionDialog: false,
|
showManageExtensionDialog: false,
|
||||||
|
showExtensionDetailsDialog: false,
|
||||||
showDropDbDialog: false,
|
showDropDbDialog: false,
|
||||||
showPayToEnableDialog: false,
|
showPayToEnableDialog: false,
|
||||||
dropDbExtensionId: '',
|
dropDbExtensionId: '',
|
||||||
selectedExtension: null,
|
selectedExtension: null,
|
||||||
|
selectedImage: null,
|
||||||
|
selectedExtensionDetails: null,
|
||||||
selectedExtensionRepos: null,
|
selectedExtensionRepos: null,
|
||||||
selectedRelease: null,
|
selectedRelease: null,
|
||||||
uninstallAndDropDb: false,
|
uninstallAndDropDb: false,
|
||||||
|
|
@ -812,6 +995,11 @@
|
||||||
)
|
)
|
||||||
.filter(e => (tab === 'featured' ? e.isFeatured : true))
|
.filter(e => (tab === 'featured' ? e.isFeatured : true))
|
||||||
.filter(extensionNameContains(term))
|
.filter(extensionNameContains(term))
|
||||||
|
.map(e => ({
|
||||||
|
...e,
|
||||||
|
details_link:
|
||||||
|
e.installedRelease?.details_link || e.latestRelease?.details_link
|
||||||
|
}))
|
||||||
this.tab = tab
|
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) {
|
async payAndInstall(release) {
|
||||||
try {
|
try {
|
||||||
this.selectedExtension.inProgress = true
|
this.selectedExtension.inProgress = true
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ from lnbits.extension_manager import (
|
||||||
ReleasePaymentInfo,
|
ReleasePaymentInfo,
|
||||||
UserExtensionInfo,
|
UserExtensionInfo,
|
||||||
fetch_github_release_config,
|
fetch_github_release_config,
|
||||||
|
fetch_release_details,
|
||||||
fetch_release_payment_info,
|
fetch_release_payment_info,
|
||||||
get_valid_extensions,
|
get_valid_extensions,
|
||||||
)
|
)
|
||||||
|
|
@ -128,6 +129,35 @@ async def api_install_extension(
|
||||||
) from exc
|
) 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")
|
@extension_router.put("/{ext_id}/sell")
|
||||||
async def api_update_pay_to_enable(
|
async def api_update_pay_to_enable(
|
||||||
ext_id: str,
|
ext_id: str,
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ class ExplicitRelease(BaseModel):
|
||||||
warning: Optional[str]
|
warning: Optional[str]
|
||||||
info_notification: Optional[str]
|
info_notification: Optional[str]
|
||||||
critical_notification: Optional[str]
|
critical_notification: Optional[str]
|
||||||
|
details_link: Optional[str]
|
||||||
pay_link: Optional[str]
|
pay_link: Optional[str]
|
||||||
|
|
||||||
def is_version_compatible(self):
|
def is_version_compatible(self):
|
||||||
|
|
@ -58,6 +59,9 @@ class GitHubRepoRelease(BaseModel):
|
||||||
zipball_url: str
|
zipball_url: str
|
||||||
html_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):
|
class GitHubRepo(BaseModel):
|
||||||
stargazers_count: str
|
stargazers_count: str
|
||||||
|
|
@ -210,6 +214,24 @@ async def fetch_release_payment_info(
|
||||||
return None
|
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:
|
def icon_to_github_url(source_repo: str, path: Optional[str]) -> str:
|
||||||
if not path:
|
if not path:
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -315,6 +337,7 @@ class ExtensionRelease(BaseModel):
|
||||||
warning: Optional[str] = None
|
warning: Optional[str] = None
|
||||||
repo: Optional[str] = None
|
repo: Optional[str] = None
|
||||||
icon: Optional[str] = None
|
icon: Optional[str] = None
|
||||||
|
details_link: Optional[str] = None
|
||||||
|
|
||||||
pay_link: Optional[str] = None
|
pay_link: Optional[str] = None
|
||||||
cost_sats: Optional[int] = None
|
cost_sats: Optional[int] = None
|
||||||
|
|
@ -347,6 +370,7 @@ class ExtensionRelease(BaseModel):
|
||||||
archive=r.zipball_url,
|
archive=r.zipball_url,
|
||||||
source_repo=source_repo,
|
source_repo=source_repo,
|
||||||
is_github_release=True,
|
is_github_release=True,
|
||||||
|
details_link=r.details_link(source_repo),
|
||||||
repo=f"https://github.com/{source_repo}",
|
repo=f"https://github.com/{source_repo}",
|
||||||
html_url=r.html_url,
|
html_url=r.html_url,
|
||||||
)
|
)
|
||||||
|
|
@ -366,6 +390,7 @@ class ExtensionRelease(BaseModel):
|
||||||
is_version_compatible=e.is_version_compatible(),
|
is_version_compatible=e.is_version_compatible(),
|
||||||
warning=e.warning,
|
warning=e.warning,
|
||||||
html_url=e.html_url,
|
html_url=e.html_url,
|
||||||
|
details_link=e.details_link,
|
||||||
pay_link=e.pay_link,
|
pay_link=e.pay_link,
|
||||||
repo=e.repo,
|
repo=e.repo,
|
||||||
icon=e.icon,
|
icon=e.icon,
|
||||||
|
|
@ -613,18 +638,18 @@ class InstallableExtension(BaseModel):
|
||||||
repo, latest_release, config = await fetch_github_repo_info(
|
repo, latest_release, config = await fetch_github_repo_info(
|
||||||
github_release.organisation, github_release.repository
|
github_release.organisation, github_release.repository
|
||||||
)
|
)
|
||||||
|
source_repo = f"{github_release.organisation}/{github_release.repository}"
|
||||||
return InstallableExtension(
|
return InstallableExtension(
|
||||||
id=github_release.id,
|
id=github_release.id,
|
||||||
name=config.name,
|
name=config.name,
|
||||||
short_description=config.short_description,
|
short_description=config.short_description,
|
||||||
stars=int(repo.stargazers_count),
|
stars=int(repo.stargazers_count),
|
||||||
icon=icon_to_github_url(
|
icon=icon_to_github_url(
|
||||||
f"{github_release.organisation}/{github_release.repository}",
|
source_repo,
|
||||||
config.tile,
|
config.tile,
|
||||||
),
|
),
|
||||||
latest_release=ExtensionRelease.from_github_release(
|
latest_release=ExtensionRelease.from_github_release(
|
||||||
repo.html_url, latest_release
|
source_repo, latest_release
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -740,6 +765,12 @@ class CreateExtension(BaseModel):
|
||||||
payment_hash: Optional[str] = None
|
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]:
|
def get_valid_extensions(include_deactivated: Optional[bool] = True) -> List[Extension]:
|
||||||
valid_extensions = [
|
valid_extensions = [
|
||||||
extension for extension in ExtensionManager().extensions if extension.is_valid
|
extension for extension in ExtensionManager().extensions if extension.is_valid
|
||||||
|
|
|
||||||
2
lnbits/static/bundle.min.js
vendored
2
lnbits/static/bundle.min.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -258,5 +258,7 @@ window.localisation.en = {
|
||||||
sell_info:
|
sell_info:
|
||||||
'The %{name} extension requires a payment of minimum %{amount} sats to enable.',
|
'The %{name} extension requires a payment of minimum %{amount} sats to enable.',
|
||||||
hide_empty_wallets: 'Hide empty wallets',
|
hide_empty_wallets: 'Hide empty wallets',
|
||||||
recheck: 'Recheck'
|
recheck: 'Recheck',
|
||||||
|
contributors: 'Contributors',
|
||||||
|
license: 'License'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue