mirror of
https://github.com/ParisNeo/lollms-webui.git
synced 2024-12-18 20:17:50 +00:00
enhanced tools
This commit is contained in:
parent
3f677b0158
commit
a2582a650b
92
app.py
92
app.py
@ -9,46 +9,78 @@ from lollms.utilities import PackageManager
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
|
||||
print("Checking ParisNeo libraries installation")
|
||||
expected_ascii_colors_version = "0.4.0"
|
||||
import threading
|
||||
import time
|
||||
import sys
|
||||
from typing import List, Tuple
|
||||
expected_ascii_colors_version = "0.4.2"
|
||||
print(f"Checking ascii_colors ({expected_ascii_colors_version}) ...", end="", flush=True)
|
||||
if not PackageManager.check_package_installed_with_version("ascii_colors", ):
|
||||
if not PackageManager.check_package_installed_with_version("ascii_colors", expected_ascii_colors_version):
|
||||
PackageManager.install_or_update("ascii_colors")
|
||||
from ascii_colors import ASCIIColors
|
||||
ASCIIColors.success("OK")
|
||||
|
||||
expected_pipmaster_version = "0.2.4"
|
||||
ASCIIColors.yellow(f"Checking pipmaster ({expected_pipmaster_version}) ...", end="", flush=True)
|
||||
if not PackageManager.check_package_installed_with_version("pipmaster", expected_pipmaster_version):
|
||||
ASCIIColors.yellow(f"Checking pipmaster ({expected_ascii_colors_version}) ...", end="", flush=True)
|
||||
if not PackageManager.check_package_installed_with_version("pipmaster", "0.2.4"):
|
||||
PackageManager.install_or_update("pipmaster")
|
||||
ASCIIColors.success("OK")
|
||||
|
||||
expected_lollmsvectordb_version = "0.7.9"
|
||||
ASCIIColors.yellow(f"Checking lollmsvectordb ({expected_lollmsvectordb_version}) ...", end="", flush=True)
|
||||
if not PackageManager.check_package_installed_with_version("lollmsvectordb", expected_lollmsvectordb_version):
|
||||
PackageManager.install_or_update("lollmsvectordb")
|
||||
ASCIIColors.success("OK")
|
||||
|
||||
expected_freedom_search_version = "0.1.7"
|
||||
ASCIIColors.yellow(f"Checking freedom_search ({expected_freedom_search_version}) ...", end="", flush=True)
|
||||
if not PackageManager.check_package_installed_with_version("freedom_search", expected_freedom_search_version):
|
||||
PackageManager.install_or_update("freedom-search")
|
||||
ASCIIColors.success("OK")
|
||||
|
||||
expected_scrapemaster_version = "0.1.6"
|
||||
ASCIIColors.yellow(f"Checking scrapemaster ({expected_scrapemaster_version}) ...", end="", flush=True)
|
||||
if not PackageManager.check_package_installed_with_version("scrapemaster", expected_scrapemaster_version):
|
||||
PackageManager.install_or_update("scrapemaster")
|
||||
ASCIIColors.success("OK")
|
||||
|
||||
expected_lollms_client_version = "0.6.2"
|
||||
ASCIIColors.yellow(f"Checking lollms_client ({expected_lollms_client_version}) ...", end="", flush=True)
|
||||
if not PackageManager.check_package_installed_with_version("lollms_client", expected_lollms_client_version):
|
||||
PackageManager.install_or_update("lollms-client")
|
||||
import pipmaster as pm
|
||||
ASCIIColors.success("OK")
|
||||
|
||||
|
||||
def animate(text: str, stop_event: threading.Event):
|
||||
animation = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
|
||||
idx = 0
|
||||
while not stop_event.is_set():
|
||||
ASCIIColors.yellow(f"\r{text} {animation[idx % len(animation)]}", end="", flush=True)
|
||||
idx += 1
|
||||
time.sleep(0.1)
|
||||
print("\r" + " " * 50, end="\r") # Clear the line
|
||||
|
||||
def check_and_install_package(package: str, version: str):
|
||||
stop_event = threading.Event()
|
||||
animation_thread = threading.Thread(target=animate, args=(f"Checking {package} ({version})", stop_event))
|
||||
animation_thread.start()
|
||||
|
||||
try:
|
||||
installed = PackageManager.check_package_installed_with_version(package, version)
|
||||
|
||||
if not installed:
|
||||
stop_event.set()
|
||||
animation_thread.join()
|
||||
print("\r" + " " * 50, end="\r") # Clear the line
|
||||
PackageManager.install_or_update(package)
|
||||
|
||||
stop_event.set()
|
||||
animation_thread.join()
|
||||
|
||||
print("\r" + " " * 50, end="\r") # Clear the line
|
||||
ASCIIColors.yellow(f"Checking {package} ({version}) ...", end="")
|
||||
ASCIIColors.success("OK")
|
||||
|
||||
except Exception as e:
|
||||
stop_event.set()
|
||||
animation_thread.join()
|
||||
print("\r" + " " * 50, end="\r") # Clear the line
|
||||
ASCIIColors.red(f"Error checking/installing {package}: {str(e)}")
|
||||
|
||||
packages: List[Tuple[str, str]] = [
|
||||
("lollmsvectordb", "0.7.9"),
|
||||
("freedom_search", "0.1.7"),
|
||||
("scrapemaster", "0.1.6"),
|
||||
("lollms_client", "0.6.2")
|
||||
]
|
||||
|
||||
def main():
|
||||
ASCIIColors.cyan("Checking ParisNeo libraries installation")
|
||||
print()
|
||||
|
||||
for package, version in packages:
|
||||
check_and_install_package(package, version)
|
||||
print() # Add a newline for better readability between package checks
|
||||
|
||||
ASCIIColors.green("All packages have been checked and are up to date!")
|
||||
|
||||
main()
|
||||
from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from starlette.responses import FileResponse
|
||||
|
2
endpoints/libraries/axios.min.js
vendored
Normal file
2
endpoints/libraries/axios.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
13
endpoints/libraries/jszip.min.js
vendored
Normal file
13
endpoints/libraries/jszip.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
62
endpoints/libraries/tailwindcss.js
Normal file
62
endpoints/libraries/tailwindcss.js
Normal file
File diff suppressed because one or more lines are too long
@ -1,3 +1,7 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi.responses import PlainTextResponse
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from fastapi.responses import PlainTextResponse
|
||||
from pydantic import BaseModel, Field
|
||||
@ -21,6 +25,21 @@ import pipmaster as pm
|
||||
if not pm.is_installed("httpx"):
|
||||
pm.install("httpx")
|
||||
import httpx
|
||||
from lollms.utilities import PackageManager
|
||||
|
||||
# Pull the repository if it already exists
|
||||
def check_lollms_models_zoo():
|
||||
if not PackageManager.check_package_installed("zipfile"):
|
||||
PackageManager.install_or_update("zipfile36")
|
||||
ASCIIColors.execute_with_animation("Checking zip library.", check_lollms_models_zoo)
|
||||
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
from pathlib import Path
|
||||
import zipfile
|
||||
import io
|
||||
|
||||
router = APIRouter()
|
||||
lollmsElfServer: LOLLMSWebUI = LOLLMSWebUI.get_instance()
|
||||
@ -136,13 +155,6 @@ async def open_folder_in_vscode(request: OpenFolderRequest):
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to open folder: {str(e)}")
|
||||
|
||||
@router.post("/apps/{app_name}/code")
|
||||
async def get_app_code(app_name: str, auth: AuthRequest):
|
||||
check_access(lollmsElfServer, auth.client_id)
|
||||
app_path = lollmsElfServer.lollms_paths.apps_zoo_path / app_name / "index.html"
|
||||
if not app_path.exists():
|
||||
raise HTTPException(status_code=404, detail="App not found")
|
||||
return FileResponse(app_path)
|
||||
|
||||
@router.get("/apps/{app_name}/{file}")
|
||||
async def get_app_file(app_name: str, file: str):
|
||||
@ -152,6 +164,100 @@ async def get_app_file(app_name: str, file: str):
|
||||
raise HTTPException(status_code=404, detail="App file not found")
|
||||
return FileResponse(app_path)
|
||||
|
||||
class AppNameInput(BaseModel):
|
||||
client_id: str
|
||||
app_name: str
|
||||
|
||||
|
||||
@router.post("/download_app")
|
||||
async def download_app(input_data: AppNameInput):
|
||||
check_access(lollmsElfServer, input_data.client_id)
|
||||
app_name = sanitize_path(input_data.app_name)
|
||||
app_path = lollmsElfServer.lollms_paths.apps_zoo_path / app_name
|
||||
|
||||
if not app_path.exists():
|
||||
raise HTTPException(status_code=404, detail="App not found")
|
||||
|
||||
# Create a BytesIO object to store the zip file
|
||||
zip_buffer = io.BytesIO()
|
||||
|
||||
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
|
||||
for file in app_path.rglob('*'):
|
||||
if file.is_file() and '.git' not in file.parts:
|
||||
relative_path = file.relative_to(app_path)
|
||||
zip_file.write(file, arcname=relative_path)
|
||||
|
||||
# Seek to the beginning of the BytesIO object
|
||||
zip_buffer.seek(0)
|
||||
|
||||
# Create a StreamingResponse to return the zip file
|
||||
return StreamingResponse(
|
||||
zip_buffer,
|
||||
media_type="application/zip",
|
||||
headers={"Content-Disposition": f"attachment; filename={app_name}.zip"}
|
||||
)
|
||||
|
||||
import os
|
||||
import yaml
|
||||
from fastapi import FastAPI, File, UploadFile, HTTPException
|
||||
from fastapi.responses import JSONResponse
|
||||
import zipfile
|
||||
import shutil
|
||||
|
||||
@router.post("/upload_app")
|
||||
async def upload_app(client_id: str, file: UploadFile = File(...)):
|
||||
check_access(lollmsElfServer, client_id)
|
||||
|
||||
# Create a temporary directory to extract the zip file
|
||||
temp_dir = lollmsElfServer.lollms_paths.personal_path / "temp"
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
|
||||
try:
|
||||
# Save the uploaded file temporarily
|
||||
temp_file = temp_dir / file.filename
|
||||
with open(temp_file, "wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
|
||||
# Extract the zip file
|
||||
with zipfile.ZipFile(temp_file, 'r') as zip_ref:
|
||||
zip_ref.extractall(temp_dir)
|
||||
|
||||
# Check for required files
|
||||
required_files = ['index.html', 'description.yaml', 'icon.png']
|
||||
for required_file in required_files:
|
||||
if not os.path.exists(os.path.join(temp_dir, required_file)):
|
||||
raise HTTPException(status_code=400, detail=f"Missing required file: {required_file}")
|
||||
|
||||
# Read the description.yaml file
|
||||
with open(os.path.join(temp_dir, 'description.yaml'), 'r') as yaml_file:
|
||||
description = yaml.safe_load(yaml_file)
|
||||
|
||||
# Get the app name from the description
|
||||
app_name = description.get('name')
|
||||
if not app_name:
|
||||
raise HTTPException(status_code=400, detail="App name not found in description.yaml")
|
||||
|
||||
# Create the app directory
|
||||
app_dir = lollmsElfServer.lollms_paths.apps_zoo_path / app_name
|
||||
if os.path.exists(app_dir):
|
||||
raise HTTPException(status_code=400, detail="An app with this name already exists")
|
||||
|
||||
# Move the extracted files to the app directory
|
||||
shutil.move(temp_dir, app_dir)
|
||||
|
||||
return JSONResponse(content={"message": f"App '{app_name}' uploaded successfully"}, status_code=200)
|
||||
|
||||
except zipfile.BadZipFile:
|
||||
raise HTTPException(status_code=400, detail="Invalid zip file")
|
||||
except yaml.YAMLError:
|
||||
raise HTTPException(status_code=400, detail="Invalid YAML in description.yaml")
|
||||
finally:
|
||||
# Clean up temporary files
|
||||
if os.path.exists(temp_file):
|
||||
os.remove(temp_file)
|
||||
if os.path.exists(temp_dir):
|
||||
shutil.rmtree(temp_dir)
|
||||
|
||||
|
||||
@router.post("/install/{app_name}")
|
||||
async def install_app(app_name: str, auth: AuthRequest):
|
||||
@ -164,9 +270,9 @@ async def install_app(app_name: str, auth: AuthRequest):
|
||||
|
||||
# Define the local paths for the files to copy
|
||||
files_to_copy = {
|
||||
"icon.png": REPO_DIR/app_name/"icon.png",
|
||||
"icon.png": REPO_DIR/app_name/"icon.png",
|
||||
"description.yaml": REPO_DIR/app_name/"description.yaml",
|
||||
"index.html": REPO_DIR/app_name/"index.html"
|
||||
"index.html": REPO_DIR/app_name/"index.html"
|
||||
}
|
||||
|
||||
# Copy each file from the local repo
|
||||
@ -250,33 +356,38 @@ def load_apps_data():
|
||||
))
|
||||
return apps
|
||||
|
||||
@router.get("/lollms_js", response_class=PlainTextResponse)
|
||||
async def lollms_js():
|
||||
# Define the path to the JSON file using pathlib
|
||||
file_path = Path(__file__).parent / "lollms_client_js.js"
|
||||
|
||||
# Read the JSON file
|
||||
with file_path.open('r') as file:
|
||||
data = file.read()
|
||||
return data
|
||||
@router.get("/lollms_assets/{asset_type}/{file_name}", response_class=PlainTextResponse)
|
||||
async def lollms_assets(asset_type: str, file_name: str):
|
||||
# Define the base path
|
||||
base_path = Path(__file__).parent
|
||||
|
||||
@router.get("/lollms_markdown_renderer", response_class=PlainTextResponse)
|
||||
async def lollms_markdown_renderer():
|
||||
# Define the path to the JSON file using pathlib
|
||||
file_path = Path(__file__).parent / "lollms_markdown_renderer.js"
|
||||
|
||||
# Read the JSON file
|
||||
with file_path.open('r') as file:
|
||||
data = file.read()
|
||||
return data
|
||||
# Determine the correct directory based on asset_type
|
||||
if asset_type == "js":
|
||||
directory = base_path / "libraries"
|
||||
file_extension = ".js"
|
||||
elif asset_type == "css":
|
||||
directory = base_path / "styles"
|
||||
file_extension = ".css"
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Invalid asset type")
|
||||
|
||||
@router.get("/lollms_markdown_renderer_css")
|
||||
async def lollms_markdown_renderer_css():
|
||||
# Define the path to the CSS file using pathlib
|
||||
file_path = Path(__file__).parent / "lollms_markdown_renderer.css"
|
||||
# Sanitize the file name to prevent path traversal
|
||||
safe_file_name = sanitize_path(file_name)
|
||||
|
||||
# Use FileResponse to serve the CSS file
|
||||
return FileResponse(file_path, media_type="text/css")
|
||||
# Construct the full file path
|
||||
file_path = directory / f"{safe_file_name}{file_extension}"
|
||||
|
||||
# Check if the file exists and is within the allowed directory
|
||||
if not file_path.is_file() or not file_path.is_relative_to(directory):
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
# Read and return the file content
|
||||
try:
|
||||
with file_path.open('r') as file:
|
||||
content = file.read()
|
||||
return content
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error reading file: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/template")
|
||||
|
9
endpoints/styles/hilight.js.default.min.css
vendored
Normal file
9
endpoints/styles/hilight.js.default.min.css
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
/*!
|
||||
Theme: Default
|
||||
Description: Original highlight.js style
|
||||
Author: (c) Ivan Sagalaev <maniac@softwaremaniacs.org>
|
||||
Maintainer: @highlightjs/core-team
|
||||
Website: https://highlightjs.org/
|
||||
License: see project LICENSE
|
||||
Touched: 2021
|
||||
*/pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#f3f3f3;color:#444}.hljs-comment{color:#697070}.hljs-punctuation,.hljs-tag{color:#444a}.hljs-tag .hljs-attr,.hljs-tag .hljs-name{color:#444}.hljs-attribute,.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-name,.hljs-selector-tag{font-weight:700}.hljs-deletion,.hljs-number,.hljs-quote,.hljs-selector-class,.hljs-selector-id,.hljs-string,.hljs-template-tag,.hljs-type{color:#800}.hljs-section,.hljs-title{color:#800;font-weight:700}.hljs-link,.hljs-operator,.hljs-regexp,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-symbol,.hljs-template-variable,.hljs-variable{color:#ab5656}.hljs-literal{color:#695}.hljs-addition,.hljs-built_in,.hljs-bullet,.hljs-code{color:#397300}.hljs-meta{color:#1f7199}.hljs-meta .hljs-string{color:#38a}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}
|
1
endpoints/styles/tailwind.min.css
vendored
Normal file
1
endpoints/styles/tailwind.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -1 +1 @@
|
||||
Subproject commit fa70a95766bbda5860af375b0d2c046414066f4a
|
||||
Subproject commit 1b4c8ca2930731f1012380e00538d8ccdf9fd816
|
@ -32,4 +32,7 @@ pipmaster>=0.1.7
|
||||
lollmsvectordb>=0.3.0
|
||||
freedom-search
|
||||
scrapemaster
|
||||
poetry
|
||||
|
||||
aiofiles
|
||||
python-multipart
|
||||
zipfile36
|
File diff suppressed because one or more lines are too long
2
web/dist/index.html
vendored
2
web/dist/index.html
vendored
@ -6,7 +6,7 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js"></script>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>LoLLMS WebUI</title>
|
||||
<script type="module" crossorigin src="/assets/index-36498a73.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-c846d59c.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index-22501ef4.css">
|
||||
</head>
|
||||
<body>
|
||||
|
@ -24,7 +24,13 @@
|
||||
</svg>
|
||||
Open Folder
|
||||
</button>
|
||||
<input type="file" @change="onFileSelected" accept=".zip" ref="fileInput" style="display: none;">
|
||||
<button @click="triggerFileInput" :disabled="isUploading" class="btn-secondary text-green-500 hover:text-green-600 transition duration-300 ease-in-out" title="Upload App">
|
||||
{{ isUploading ? 'Uploading...' : 'Upload App' }}
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="message">{{ message }}</p>
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
|
||||
<div class="relative flex-grow max-w-md">
|
||||
<input
|
||||
@ -96,7 +102,7 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button @click="downloadApp(app.name)" class="text-green-500 hover:text-green-600 transition duration-300 ease-in-out" title="Download">
|
||||
<button @click="downloadApp(app.folder_name)" class="text-green-500 hover:text-green-600 transition duration-300 ease-in-out" title="Download">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
@ -149,6 +155,10 @@ export default {
|
||||
message: '',
|
||||
successMessage: true,
|
||||
searchQuery: '',
|
||||
selectedFile: null,
|
||||
isUploading: false,
|
||||
message: '',
|
||||
error: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -175,6 +185,46 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
triggerFileInput() {
|
||||
this.$refs.fileInput.click();
|
||||
},
|
||||
onFileSelected(event) {
|
||||
this.selectedFile = event.target.files[0];
|
||||
this.message = '';
|
||||
this.error = '';
|
||||
this.uploadApp();
|
||||
},
|
||||
async uploadApp() {
|
||||
if (!this.selectedFile) {
|
||||
this.error = 'Please select a file to upload.';
|
||||
return;
|
||||
}
|
||||
|
||||
this.isUploading = true;
|
||||
this.message = '';
|
||||
this.error = '';
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', this.selectedFile);
|
||||
formData.append('client_id', this.$store.state.client_id);
|
||||
|
||||
try {
|
||||
const response = await axios.post('/upload_app', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
|
||||
this.message = response.data.message;
|
||||
this.$refs.fileInput.value = ''; // Reset file input
|
||||
this.selectedFile = null;
|
||||
} catch (error) {
|
||||
console.error('Error uploading app:', error);
|
||||
this.error = error.response?.data?.detail || 'Failed to upload the app. Please try again.';
|
||||
} finally {
|
||||
this.isUploading = false;
|
||||
}
|
||||
},
|
||||
async fetchApps() {
|
||||
this.loading = true;
|
||||
try {
|
||||
@ -283,20 +333,43 @@ export default {
|
||||
}
|
||||
},
|
||||
async downloadApp(appName) {
|
||||
this.isLoading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
const response = await axios.get(`/download/${appName}`, {
|
||||
responseType: 'blob',
|
||||
const response = await axios.post('/download_app', {
|
||||
client_id: this.$store.state.client_id,
|
||||
app_name: appName
|
||||
});
|
||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', `${appName}.zip`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
this.showMessage('Download started!', true);
|
||||
|
||||
// Get the filename from the Content-Disposition header
|
||||
const contentDisposition = response.headers['content-disposition'];
|
||||
const filenameMatch = contentDisposition && contentDisposition.match(/filename="?(.+)"?/i);
|
||||
const filename = filenameMatch ? filenameMatch[1] : 'app.zip';
|
||||
|
||||
// Create a Blob from the response data
|
||||
const blob = new Blob([response.data], { type: 'application/zip' });
|
||||
|
||||
// Create a temporary URL for the Blob
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
|
||||
// Create a temporary anchor element and trigger the download
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
// Clean up
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
|
||||
} catch (error) {
|
||||
this.showMessage('Download failed.', false);
|
||||
console.error('Error downloading app:', error);
|
||||
this.error = 'Failed to download the app. Please try again.';
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
openApp(app) {
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 70cc5e9c506d73c392bb123402106b5df07eac90
|
||||
Subproject commit dfaad7d9e9c973b41f99d2faeb938bfd7d8bf9e6
|
Loading…
Reference in New Issue
Block a user