mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2024-11-19 01:43:42 +01:00
[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
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
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:
|
||||
'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'
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user