madiator-docker-runpod/official-templates/better-ai-launcher/app/templates/index.html

2527 lines
96 KiB
HTML
Raw Normal View History

2024-10-12 14:46:41 +02:00
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Better App Launcher</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
body, html {
font-family: Arial, sans-serif;
background-color: #1a1a1a;
color: #ffffff;
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
}
.container {
display: flex;
height: 100vh;
}
.apps-section {
width: 40%;
overflow-y: auto;
padding: 20px;
box-sizing: border-box;
}
.logs-section {
width: 60%;
padding: 20px;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.app {
background-color: #2a2a2a;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.app h2 {
margin-top: 0;
margin-bottom: 15px;
font-size: 1.4em;
}
.button-group {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin-bottom: 10px;
}
button {
border: none;
color: white;
padding: 12px 15px;
text-align: center;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 14px;
cursor: pointer;
border-radius: 5px;
transition: background-color 0.3s, transform 0.1s;
width: 100%;
}
button:hover:not(:disabled) {
transform: translateY(-2px);
}
button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
button i {
margin-right: 5px;
}
.start-button {
background-color: #4CAF50;
}
.start-button:hover:not(:disabled) {
background-color: #45a049;
}
.stop-button {
background-color: #f44336;
}
.stop-button:hover:not(:disabled) {
background-color: #da190b;
}
.log-button {
background-color: #008CBA;
}
.log-button:hover:not(:disabled) {
background-color: #007aa3;
}
.open-button {
background-color: #FF9800;
}
.open-button:hover:not(:disabled) {
background-color: #e68a00;
}
.install-button {
background-color: #9C27B0;
grid-column: span 2;
}
.install-button:hover {
background-color: #7B1FA2;
}
.force-kill-button {
background-color: #d9534f;
}
.force-kill-button:hover:not(:disabled) {
background-color: #c9302c;
}
#logs {
background-color: #2a2a2a;
border-radius: 10px;
padding: 15px;
height: calc(100% - 50px);
overflow-y: auto;
font-family: monospace;
white-space: pre-wrap;
word-wrap: break-word;
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.1);
}
.error-message {
color: #ff6b6b;
margin-top: 10px;
margin-bottom: 15px;
font-style: italic;
}
h1 {
margin-top: 0;
font-size: 2em;
margin-bottom: 20px;
}
#currentAppName {
margin-bottom: 10px;
font-size: 1.2em;
}
.status {
display: inline-block;
padding: 5px 10px;
border-radius: 15px;
font-size: 0.9em;
font-weight: bold;
margin-top: 10px;
}
.status-running {
background-color: #4CAF50;
}
.status-stopped {
background-color: #f44336;
}
/* New styles for navbar and tabs */
.navbar {
background-color: #2a2a2a;
padding: 10px 20px;
display: flex;
justify-content: space-between;
align-items: center;
2024-10-21 11:03:33 +02:00
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
2024-10-12 14:46:41 +02:00
}
.navbar-brand {
display: flex;
align-items: center;
}
.navbar-logo {
width: 40px;
height: 40px;
margin-right: 15px;
}
.navbar-title {
color: #ffffff;
font-size: 1.2em;
font-weight: bold;
}
.navbar-disclaimer {
color: #888;
font-size: 0.8em;
font-style: italic;
margin-top: 5px;
}
.navbar-tabs {
display: flex;
}
.navbar-tabs a {
color: #ffffff;
text-decoration: none;
padding: 10px 15px;
margin-left: 10px;
border-radius: 5px;
transition: background-color 0.3s;
}
.navbar-tabs a:hover {
background-color: #3a3a3a;
}
.navbar-tabs a.active {
background-color: #4CAF50;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
#killAllButton {
background-color: #d9534f;
color: white;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
margin-bottom: 20px;
}
#killAllButton:hover {
background-color: #c9302c;
}
/* Updated styles for layout */
body, html {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
.main-container {
display: flex;
flex-direction: column;
height: 100vh;
}
.navbar {
background-color: #333;
padding: 10px 0;
}
.content-container {
display: flex;
flex: 1;
overflow: hidden;
}
#apps-tab {
display: flex;
width: 100%;
height: 100%;
}
.apps-section {
width: 40%;
height: 100%;
overflow-y: auto;
padding: 20px;
box-sizing: border-box;
}
.logs-section {
width: 60%;
height: 100%;
padding: 20px;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
#logs-container {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
2024-10-21 11:03:33 +02:00
height: calc(100% - 50px); /* Adjust this value based on your layout */
2024-10-12 14:46:41 +02:00
}
#logs {
flex: 1;
overflow-y: auto;
background-color: #2a2a2a;
border-radius: 10px;
padding: 15px;
font-family: monospace;
white-space: pre-wrap;
word-wrap: break-word;
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.1);
margin-bottom: 60px; /* Space for the download button */
max-height: 100%; /* Ensure it doesn't exceed the container height */
}
.download-logs-btn {
position: absolute;
bottom: 10px;
right: 10px;
background-color: #17a2b8;
color: white;
border: none;
border-radius: 50%;
width: 50px;
height: 50px;
font-size: 24px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.3s;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
#settings-tab {
padding: 20px;
box-sizing: border-box;
overflow-y: auto;
height: 100%;
}
.logo-title-container {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.logo {
width: 50px;
height: 50px;
margin-right: 15px;
}
.logo-title-container div {
display: flex;
flex-direction: column;
}
h1 {
margin: 0;
font-size: 2em;
}
.disclaimer {
margin: 5px 0 0;
font-size: 0.9em;
color: #888;
font-style: italic;
}
.copyright {
text-align: center;
padding: 10px;
font-size: 0.8em;
color: #888;
position: absolute;
bottom: 0;
width: 100%;
background-color: #1a1a1a;
}
.install-container {
margin-top: 10px;
}
.install-progress {
display: none;
margin-top: 10px;
}
.progress-bar {
width: 100%;
height: 20px;
background-color: #ddd;
border-radius: 10px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background-color: #4CAF50;
transition: width 0.5s ease-in-out;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 12px;
}
.download-info {
margin-top: 5px;
font-size: 0.9em;
display: flex;
justify-content: space-between;
}
.install-logs {
margin-top: 10px;
max-height: 100px;
overflow-y: auto;
font-family: monospace;
font-size: 12px;
background-color: #222;
padding: 5px;
border-radius: 3px;
display: none;
}
.install-stage {
margin-top: 5px;
font-size: 0.9em;
font-weight: bold;
}
#loadingOverlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 1000;
}
.spinner {
border: 5px solid #f3f3f3;
border-top: 5px solid #3498db;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin-bottom: 20px;
}
#loadingMessage {
color: white;
font-size: 18px;
text-align: center;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.button-group button,
.fix-custom-nodes-button {
border: none;
color: white;
padding: 12px 15px;
text-align: center;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 14px;
cursor: pointer;
border-radius: 5px;
transition: background-color 0.3s, transform 0.1s;
width: 100%;
margin-top: 10px;
}
.fix-custom-nodes-button {
background-color: #9C27B0;
}
.fix-custom-nodes-button:hover {
background-color: #7B1FA2;
transform: translateY(-2px);
}
.fix-custom-nodes-button i {
margin-right: 5px;
}
.progress-container {
margin-top: 10px;
}
.progress-bar {
width: 100%;
height: 20px;
background-color: #ddd;
border-radius: 10px;
overflow: hidden;
margin-bottom: 5px;
}
.progress-bar-fill {
height: 100%;
background-color: #4CAF50;
transition: width 0.5s ease-in-out;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 12px;
}
.progress-label {
font-size: 0.9em;
margin-bottom: 5px;
}
2024-10-21 11:03:33 +02:00
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.4);
}
.modal-content {
background-color: #2a2a2a;
margin: 15% auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
max-width: 600px;
border-radius: 5px;
}
.modal h3 {
color: #4CAF50;
margin-top: 0;
}
.modal ul {
list-style-type: none;
padding: 0;
}
.modal li {
margin-bottom: 10px;
}
.modal button {
background-color: #4CAF50;
color: white;
padding: 10px 15px;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s;
}
.modal button:hover {
background-color: #45a049;
}
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
}
.close:hover,
.close:focus {
color: #fff;
text-decoration: none;
cursor: pointer;
}
2024-10-12 14:46:41 +02:00
.app {
background-color: #2a2a2a;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.button-group {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin-bottom: 10px;
}
.button-group button,
.fix-custom-nodes-button,
.install-button {
border: none;
color: white;
padding: 12px 15px;
text-align: center;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 14px;
cursor: pointer;
border-radius: 5px;
transition: background-color 0.3s, transform 0.1s;
width: 100%;
margin-top: 10px;
}
.install-logs,
.install-progress {
margin-top: 10px;
width: 100%;
}
.install-logs {
max-height: 150px;
overflow-y: auto;
font-family: monospace;
font-size: 12px;
background-color: #222;
padding: 10px;
border-radius: 5px;
color: #fff;
}
#poddy {
display: none;
position: fixed;
bottom: 20px;
right: 20px;
width: 100px;
height: 100px;
background-image: url('/static/poddy.png');
background-size: contain;
background-repeat: no-repeat;
animation: dance 1s infinite;
}
@keyframes dance {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-20px) rotate(10deg); }
}
#poddy-animation {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: #000;
z-index: 9999;
overflow: hidden;
}
.poddy {
position: absolute;
width: 15vmin;
height: 15vmin;
background-image: url('/static/poddy.png');
background-size: contain;
background-repeat: no-repeat;
transition: all 0.5s ease-in-out;
}
#special-item {
position: absolute;
width: 30vmin;
height: 30vmin;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
@keyframes dance {
0%, 100% { transform: translateY(0) rotate(0deg); }
25% { transform: translateY(-2vmin) rotate(-5deg); }
75% { transform: translateY(-2vmin) rotate(5deg); }
}
@keyframes wiggle {
0%, 100% { transform: rotate(0deg); }
25% { transform: rotate(-5deg); }
75% { transform: rotate(5deg); }
}
.download-log-button {
background-color: #17a2b8;
}
.download-log-button:hover:not(:disabled) {
background-color: #138496;
}
#logs-container {
position: relative;
flex: 1;
display: flex;
flex-direction: column;
}
#logs {
flex: 1;
overflow-y: auto;
padding-bottom: 70px; /* Adjust this value to match the button size + padding */
}
.download-logs-btn {
position: absolute;
bottom: 10px; /* Reduced from 20px to 10px */
right: 10px; /* Reduced from 20px to 10px */
background-color: #17a2b8;
color: white;
border: none;
border-radius: 50%;
width: 50px;
height: 50px;
font-size: 24px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.3s;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
z-index: 10;
padding: 0;
}
.download-logs-btn i {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.download-logs-btn:hover {
background-color: #138496;
}
.settings-container {
padding: 20px;
2024-10-21 11:03:33 +02:00
width: 100%;
box-sizing: border-box;
2024-10-12 14:46:41 +02:00
}
.settings-grid {
display: flex;
flex-wrap: wrap;
gap: 20px;
2024-10-21 11:03:33 +02:00
justify-content: space-between;
2024-10-12 14:46:41 +02:00
}
.setting-group {
2024-10-21 11:03:33 +02:00
flex: 1 1 300px;
min-width: 300px;
2024-10-12 14:46:41 +02:00
background-color: #2a2a2a;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
}
.setting-group h3 {
margin-top: 0;
margin-bottom: 15px;
2024-10-21 11:03:33 +02:00
color: #4CAF50;
2024-10-12 14:46:41 +02:00
}
2024-10-21 11:03:33 +02:00
.ssh-details, .ssh-password-form {
2024-10-12 14:46:41 +02:00
background-color: #333;
border-radius: 5px;
padding: 15px;
margin-top: 10px;
}
.settings-button {
background-color: #4CAF50;
color: white;
padding: 10px 15px;
border: none;
border-radius: 5px;
cursor: pointer;
margin-right: 10px;
margin-bottom: 10px;
2024-10-21 11:03:33 +02:00
transition: background-color 0.3s;
2024-10-12 14:46:41 +02:00
}
.settings-button:hover {
background-color: #45a049;
}
#newSshPassword {
2024-10-21 11:03:33 +02:00
width: 100%;
2024-10-12 14:46:41 +02:00
padding: 10px;
margin-bottom: 10px;
2024-10-21 11:03:33 +02:00
box-sizing: border-box;
border: 1px solid #444;
border-radius: 5px;
background-color: #333;
color: #fff;
2024-10-12 14:46:41 +02:00
}
.password-buttons {
display: flex;
justify-content: space-between;
}
.ssh-security-notice {
font-size: 0.8em;
color: #888;
margin-top: 10px;
font-style: italic;
}
2024-10-21 11:03:33 +02:00
#filebrowser-status {
display: inline-block;
padding: 5px 10px;
border-radius: 15px;
font-size: 0.9em;
font-weight: bold;
margin: 10px 0;
background-color: #333;
2024-10-12 14:46:41 +02:00
}
2024-10-21 11:03:33 +02:00
/* Add these new styles for the model downloader */
.model-downloader {
display: flex;
flex-direction: column;
gap: 10px;
2024-10-12 14:46:41 +02:00
}
2024-10-21 11:03:33 +02:00
.model-downloader input,
.model-downloader select,
.model-downloader button {
2024-10-12 14:46:41 +02:00
width: 100%;
padding: 10px;
2024-10-21 11:03:33 +02:00
margin-bottom: 10px;
box-sizing: border-box;
2024-10-12 14:46:41 +02:00
border: 1px solid #444;
border-radius: 5px;
background-color: #333;
color: #fff;
}
2024-10-21 11:03:33 +02:00
.model-downloader button {
background-color: #4CAF50;
color: white;
border: none;
cursor: pointer;
transition: background-color 0.3s;
}
.model-downloader button:hover {
background-color: #45a049;
}
.example-urls {
2024-10-12 14:46:41 +02:00
display: flex;
2024-10-21 11:03:33 +02:00
flex-direction: column;
gap: 10px;
}
.example-url {
display: flex;
align-items: center;
}
.example-label {
flex: 0 0 150px;
font-weight: bold;
color: #4CAF50;
2024-10-12 14:46:41 +02:00
}
2024-10-21 11:03:33 +02:00
.example-link {
2024-10-12 14:46:41 +02:00
flex: 1;
2024-10-21 11:03:33 +02:00
font-family: monospace;
background-color: #444;
padding: 5px 10px;
border-radius: 3px;
word-break: break-all;
color: #fff;
text-decoration: none;
transition: background-color 0.3s;
2024-10-12 14:46:41 +02:00
}
2024-10-21 11:03:33 +02:00
.model-folders-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
2024-10-12 14:46:41 +02:00
}
2024-10-21 11:03:33 +02:00
.model-folder {
background-color: #333;
2024-10-12 14:46:41 +02:00
border-radius: 5px;
2024-10-21 11:03:33 +02:00
padding: 15px;
2024-10-12 14:46:41 +02:00
}
2024-10-21 11:03:33 +02:00
#model-download-progress {
margin-top: 20px;
min-height: 100px;
2024-10-12 14:46:41 +02:00
}
2024-10-21 11:03:33 +02:00
.progress-bar {
width: 100%;
height: 20px;
background-color: #444;
border-radius: 10px;
overflow: hidden;
position: relative;
}
.progress-bar-fill {
height: 100%;
background-color: #4CAF50;
transition: width 0.3s ease-out;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 12px;
}
/* lutzapps - allow word-wrap for model-download-status to see downloaded file at status: 'Complete'
/*#model-download-status,*/
2024-10-21 11:03:33 +02:00
#model-download-speed,
#model-download-eta {
margin-top: 5px;
height: 20px;
line-height: 20px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#model-download-status {
margin-top: 5px;
height: 20px;
line-height: 20px;
word-wrap: break-word;
}
2024-10-21 11:03:33 +02:00
/* Update the CSS for the token saving textbox */
#civitaiTokenSave {
width: 100%;
2024-10-12 14:46:41 +02:00
padding: 10px;
2024-10-21 11:03:33 +02:00
margin-bottom: 10px;
box-sizing: border-box;
2024-10-12 14:46:41 +02:00
border: 1px solid #444;
border-radius: 5px;
background-color: #333;
color: #fff;
}
2024-10-21 11:03:33 +02:00
.example-urls {
background-color: #333;
border-radius: 5px;
padding: 15px;
2024-10-21 11:03:33 +02:00
}
.example-url {
2024-10-12 14:46:41 +02:00
display: flex;
2024-10-21 11:03:33 +02:00
align-items: center;
margin-bottom: 10px;
2024-10-12 14:46:41 +02:00
}
2024-10-21 11:03:33 +02:00
.example-label {
flex: 0 0 150px;
font-weight: bold;
color: #4CAF50;
2024-10-12 14:46:41 +02:00
}
2024-10-21 11:03:33 +02:00
.example-link {
flex: 1;
font-family: monospace;
background-color: #444;
padding: 5px 10px;
border-radius: 3px;
word-break: break-all;
color: #fff;
text-decoration: none;
transition: background-color 0.3s;
}
.example-link:hover {
background-color: #555;
}
.model-folders-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
2024-10-21 11:03:33 +02:00
}
.model-folder {
background-color: #333;
border-radius: 5px;
padding: 15px;
}
#model-download-progress {
margin-top: 20px;
}
/* lutzapps - double definition
2024-10-21 11:03:33 +02:00
#model-download-status,
#model-download-speed,
#model-download-eta {
margin-top: 10px;
} */
2024-10-21 11:03:33 +02:00
#recreate-symlinks-container {
margin-top: 20px;
}
#recreate-symlinks-container .settings-button {
width: 100%;
padding: 10px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s;
}
#recreate-symlinks-container .settings-button:hover {
background-color: #45a049;
}
#symlink-status {
2024-10-12 14:46:41 +02:00
margin-top: 10px;
font-style: italic;
}
2024-10-21 11:03:33 +02:00
@keyframes wiggle {
0%, 100% { transform: rotate(0deg); }
25% { transform: rotate(-5deg); }
75% { transform: rotate(5deg); }
}
.apps-container {
display: flex;
width: 100%;
height: 100%;
}
.apps-section {
flex: 1;
height: 100%;
overflow-y: auto;
padding: 20px;
box-sizing: border-box;
}
.logs-section {
flex: 1;
height: 100%;
padding: 20px;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
#logs-container {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
}
#logs {
flex: 1;
overflow-y: auto;
background-color: #2a2a2a;
border-radius: 10px;
padding: 15px;
font-family: monospace;
white-space: pre-wrap;
word-wrap: break-word;
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.1);
margin-bottom: 60px;
}
2024-10-12 14:46:41 +02:00
</style>
</head>
<body>
<div id="loadingOverlay">
<div class="spinner"></div>
<div id="loadingMessage">Connecting to WebSocket...</div>
</div>
<div class="main-container">
<div class="navbar">
<div class="navbar-brand">
<svg class="navbar-logo" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<path d="M74.5 51.1c-25.4 14.9-27 16-29.6 20.2-1.8 3-1.9 5.3-1.9 32.3 0 21.7.3 29.4 1.3 30.6 1.9 2.5 46.7 27.9 48.5 27.6 1.5-.3 1.7-3.1 2-27.7.2-21.9 0-27.8-1.1-29.5-.8-1.2-9.9-6.8-20.2-12.6-10.3-5.8-19.4-11.5-20.2-12.7-1.8-2.6-.9-5.9 1.8-7.4 1.6-.8 6.3 0 21.8 4C87.8 78.7 98 81 99.6 81c4.4 0 49.9-25.9 49.9-28.4 0-1.6-3.4-2.8-24-8.2-13.2-3.5-25.1-6.3-26.5-6.3-1.4.1-12.4 5.9-24.5 13z" fill="#ffffff"/>
<path d="m137.2 68.1-3.3 2.1 6.3 3.7c3.5 2 6.3 4.3 6.3 5.1 0 .9-8 6.1-19.4 12.6-10.6 6-20 11.9-20.7 12.9-1.2 1.6-1.4 7.2-1.2 29.4.3 24.8.5 27.6 2 27.9 1.8.3 46.6-25.1 48.6-27.6.9-1.2 1.2-8.8 1.2-30.2s-.3-29-1.2-30.2c-1.6-1.9-12.1-7.8-13.9-7.8-.8 0-2.9 1-4.7 2.1z" fill="#ffffff"/>
</svg>
<div>
<div class="navbar-title">RunPod Better App Manager</div>
<div class="navbar-disclaimer">This template is designed for use with RunPod Network Storage</div>
</div>
</div>
<div class="navbar-tabs">
<a href="#" class="tab-link active" onclick="openTab(event, 'apps-tab')">Apps</a>
2024-10-21 11:03:33 +02:00
<a href="#" class="tab-link" onclick="openTab(event, 'models-tab')">Models</a>
2024-10-12 14:46:41 +02:00
<a href="#" class="tab-link" onclick="openTab(event, 'settings-tab')">Settings</a>
</div>
</div>
<div class="content-container">
<div id="apps-tab" class="tab-content active">
2024-10-21 11:03:33 +02:00
<div class="apps-container">
<div class="apps-section">
<div id="apps">
{% for app_key, app_info in apps.items() %}
<div class="app">
<h2>{{ app_info.name }}</h2>
{% if app_status[app_key]['dirs_ok'] %}
<div class="button-group">
<button onclick="startApp('{{ app_key }}')" id="start-{{ app_key }}" class="start-button" {% if app_status[app_key]['status'] == 'running' %}disabled{% endif %}>
<i class="fas fa-play"></i> Start
2024-10-12 14:46:41 +02:00
</button>
2024-10-21 11:03:33 +02:00
<button onclick="stopApp('{{ app_key }}')" id="stop-{{ app_key }}" class="stop-button" {% if app_status[app_key]['status'] == 'stopped' %}disabled{% endif %}>
<i class="fas fa-stop"></i> Stop
2024-10-12 14:46:41 +02:00
</button>
2024-10-21 11:03:33 +02:00
<button onclick="viewLogs('{{ app_key }}', '{{ app_info.name }}')" class="log-button">
<i class="fas fa-list-alt"></i> View Logs
</button>
<button onclick="openApp('{{ app_key }}', {{ app_status[app_key]['port'] }})" id="open-{{ app_key }}" class="open-button" {% if app_status[app_key]['status'] == 'stopped' %}disabled{% endif %}>
<i class="fas fa-external-link-alt"></i> Open App
</button>
<button onclick="forceKillApp('{{ app_key }}')" id="force-kill-{{ app_key }}" class="force-kill-button">
<i class="fas fa-skull-crossbones"></i> Force Kill
</button>
{% if app_status[app_key]['dirs_ok'] and app_status[app_key]['is_bcomfy'] %}
<button onclick="fixCustomNodes('{{ app_key }}')" id="fix-custom-nodes-{{ app_key }}" class="fix-custom-nodes-button">
<i class="fas fa-wrench"></i> Fix Custom Nodes
</button>
{% endif %}
</div>
<span id="status-{{ app_key }}" class="status status-{{ app_status[app_key]['status'] }}">
{{ app_status[app_key]['status'] | capitalize }}
</span>
{% else %}
<p class="error-message">{{ app_status[app_key]['message'] }}</p>
{% if not app_status[app_key]['installed'] %}
<div class="install-container">
<button onclick="installApp('{{ app_key }}')" id="install-{{ app_key }}" class="install-button" {% if app_status[app_key]['install_status']['status'] == 'in_progress' %}disabled{% endif %}>
<i class="fas fa-download"></i> {% if app_status[app_key]['install_status']['status'] == 'in_progress' %}Installing...{% else %}Install {{ app_info.name }}{% endif %}
</button>
<div id="install-progress-{{ app_key }}" class="install-progress" {% if app_status[app_key]['install_status']['status'] == 'in_progress' %}style="display: block;"{% endif %}>
<div class="progress-container">
<div class="progress-label">Download Progress:</div>
<div class="progress-bar">
<div class="progress-bar-fill download-progress" style="width: 0%">0%</div>
</div>
2024-10-12 14:46:41 +02:00
</div>
2024-10-21 11:03:33 +02:00
<div class="progress-container">
<div class="progress-label">Unpack Progress:</div>
<div class="progress-bar">
<div class="progress-bar-fill unpack-progress" style="width: 0%">0%</div>
</div>
2024-10-12 14:46:41 +02:00
</div>
2024-10-21 11:03:33 +02:00
<div class="download-info">
<span class="download-speed"></span>
<span class="download-eta"></span>
</div>
<div class="install-stage"></div>
2024-10-12 14:46:41 +02:00
</div>
</div>
2024-10-21 11:03:33 +02:00
<div id="install-logs-{{ app_key }}" class="install-logs"></div>
{% endif %}
2024-10-12 14:46:41 +02:00
{% endif %}
2024-10-21 11:03:33 +02:00
</div>
{% endfor %}
2024-10-12 14:46:41 +02:00
</div>
</div>
2024-10-21 11:03:33 +02:00
<div class="logs-section">
<h2 id="currentAppName">Logs</h2>
<div id="logs-container">
<div id="logs"></div>
<button id="downloadLogsBtn" class="download-logs-btn" title="Download Logs" style="display: none;">
<i class="fas fa-download"></i>
</button>
</div>
2024-10-12 14:46:41 +02:00
</div>
</div>
</div>
<div id="settings-tab" class="tab-content" style="display: none;">
<div class="settings-container">
<div class="settings-grid">
<div class="setting-group">
2024-10-21 11:03:33 +02:00
<h3>SSH Configuration</h3>
2024-10-12 14:46:41 +02:00
<div class="ssh-details">
2024-10-21 11:03:33 +02:00
<p>IP: <span id="sshIp"></span></p>
<p>Port: <span id="sshPort"></span></p>
<p>Command: <span id="sshCommand"></span></p>
2024-10-12 14:46:41 +02:00
<button onclick="copySshCommand()" class="settings-button">Copy SSH Command</button>
2024-10-21 11:03:33 +02:00
</div>
<div class="ssh-password-form">
<input type="password" id="newSshPassword" placeholder="Enter new SSH password">
<div class="password-buttons">
<button onclick="setCustomSshPassword()" class="settings-button">Set Password</button>
<button onclick="togglePasswordVisibility()" class="settings-button">Show/Hide</button>
2024-10-12 14:46:41 +02:00
</div>
</div>
2024-10-21 11:03:33 +02:00
<p class="ssh-security-notice">Note: Password-based SSH authentication is less secure than key-based authentication.</p>
2024-10-12 14:46:41 +02:00
</div>
2024-10-21 11:03:33 +02:00
2024-10-12 14:46:41 +02:00
<div class="setting-group">
<h3>File Browser</h3>
2024-10-21 11:03:33 +02:00
<button onclick="openFileBrowser()" class="settings-button">Open File Browser</button>
2024-10-12 14:46:41 +02:00
<p>Status: <span id="filebrowser-status">Checking...</span></p>
2024-10-21 11:03:33 +02:00
<button id="start-filebrowser" onclick="controlFileBrowser('start')" class="settings-button">Start File Browser</button>
<button id="stop-filebrowser" onclick="controlFileBrowser('stop')" class="settings-button">Stop File Browser</button>
2024-10-12 14:46:41 +02:00
</div>
2024-10-21 11:03:33 +02:00
2024-10-12 14:46:41 +02:00
<div class="setting-group">
<h3>Shared Model Folders and Symlinks</h3>
<p>Manage shared model folders and symlinks across all apps.</p>
<button onclick="createSharedFolders()" class="settings-button">Create Shared Folders</button>
<button onclick="recreateSymlinks()" class="settings-button">Recreate Symlinks</button>
<p id="shared-folders-status"></p>
<p id="symlink-status"></p>
</div>
</div>
</div>
</div>
2024-10-21 11:03:33 +02:00
<div id="models-tab" class="tab-content">
<div class="settings-container">
<div class="settings-grid">
<div class="setting-group">
<h3>Model Downloader</h3>
<div class="model-downloader">
<input type="text" id="modelUrl" placeholder="Enter Civitai or Hugging Face URL">
<input type="text" id="modelName" placeholder="Enter model name (optional)">
<select id="modelType"><!-- lutzapps - Change #1 - remove default modelType select option values -->
<!-- <option value="Stable-diffusion">Checkpoint (Stable Diffusion)</option> -->
<!-- <option value="Lora">LoRA</option> -->
<!-- <option value="VAE">VAE</option> -->
<!-- <option value="ESRGAN">Upscaler</option> -->
2024-10-21 11:03:33 +02:00
</select>
<input type="password" id="civitaiToken" placeholder="Civitai API Token">
<input type="password" id="hfToken" placeholder="Hugging Face API Token (optional)">
2024-10-21 11:03:33 +02:00
<button onclick="downloadModel()" class="settings-button">Download Model</button>
</div>
<div id="model-download-progress" style="display: none;">
<div class="progress-bar">
<div class="progress-bar-fill" style="width: 0%">0%</div>
</div>
<div id="model-download-status"></div>
<div id="model-download-speed"></div>
<div id="model-download-eta"></div>
</div>
</div>
<div class="setting-group">
<h3>Civitai API Token</h3>
<div class="model-downloader">
<input type="password" id="civitaiTokenSave" placeholder="Enter Civitai API Token">
<button onclick="saveCivitaiToken()" class="settings-button">Save Token</button>
</div>
<p id="civitaiTokenStatus"></p>
</div>
<div class="setting-group">
<h3>Download Examples</h3>
<div class="example-urls">
<div class="example-url">
<span class="example-label">Stable Diffusion:</span>
<a href="#" class="example-link" id="example-sd" onclick="useInModelDownloader('ckpt', this.textContent); return false;"></a>
2024-10-21 11:03:33 +02:00
</div>
<div class="example-url">
<span class="example-label">LoRA:</span>
<a href="#" class="example-link" id="example-lora" onclick="useInModelDownloader('loras', this.textContent); return false;"></a>
2024-10-21 11:03:33 +02:00
</div>
<div class="example-url">
<span class="example-label">VAE:</span>
<a href="#" class="example-link" id="example-vae" onclick="useInModelDownloader('vae', this.textContent); return false;"></a>
2024-10-21 11:03:33 +02:00
</div>
<div class="example-url">
<span class="example-label">Upscaler:</span>
<a href="#" class="example-link" id="example-upscaler" onclick="useInModelDownloader('upscale_models', this.textContent); return false;"></a>
2024-10-21 11:03:33 +02:00
</div>
<div class="example-url">
<span class="example-label">Flux Dev:</span>
<a href="#" class="example-link" id="example-flux-dev" onclick="useInModelDownloader('unet', this.textContent); return false;"></a>
2024-10-21 11:03:33 +02:00
</div>
<div class="example-url">
<span class="example-label">Flux Schnell:</span>
<a href="#" class="example-link" id="example-flux-schnell" onclick="useInModelDownloader('unet', this.textContent); return false;"></a>
2024-10-21 11:03:33 +02:00
</div>
</div>
</div>
</div>
<div class="setting-group" style="width: 100%;">
<h3>Existing Models</h3>
<div id="model-folders" class="model-folders-grid"></div>
</div>
<div id="recreate-symlinks-container" style="display: none;"></div>
</div>
</div>
2024-10-12 14:46:41 +02:00
</div>
</div>
<div class="copyright">
&copy; 2024 RunPod Better App Manager. Created by Madiator2011.
</div>
<div id="poddy"></div>
<!-- Add this just before the closing </body> tag -->
<div id="poddy-animation" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: #000; z-index: 9999;">
<div id="poddy-container"></div>
<div id="special-item" style="display: none;"></div>
<audio id="poddy-audio" loop>
<source src="/static/poddy-song.mp3" type="audio/mpeg">
</audio>
</div>
<script>
2024-10-21 11:03:33 +02:00
const appStatuses = {};
let currentLogApp = null;
let currentLogAppName = null;
const podId = '{{ pod_id }}';
const WS_PORT = 7222; // This is the Nginx port
// lutzapps - remember the last know MODELTYPE_SELECTED of the WebUI Dom Id 'modelType' "select dropdown" model list
// so the selection can be restored between "Tab Switches", and during refreshes/reloads of the modelType list
// this is handled by the extendUIHelper() function, which is called directly from JavaScript events,
// but is also called indirectly from Python code via WS message type='extend_ui_helper'
// e.g. from the model_utils:download_civitai_model() function, to preserve "state" of the selected modelType
let MODELTYPE_SELECTED = "";
modelType.onchange = function() {
MODELTYPE_SELECTED = this.value;
alert("onchange=" + MODELTYPE_SELECTED);
}
// *** lutzapps - Change #2 - support to run locally at http://localhost:${WS_PORT} (3 locations in "index.html")
const enable_unsecure_localhost = '{{ enable_unsecure_localhost }}';
// default is to use the "production" WeckSockets CloudFlare URL
// NOTE: ` (back-ticks) are used here for template literals
var WS_URL = `wss://${podId}-${WS_PORT}.proxy.runpod.net/ws`; // need to be declared as var
if (`${enable_unsecure_localhost}` === 'True') { // value of LOCAL_DEBUG ENV var
// make sure to use "ws" Protocol (insecure) instead of "wss" (WebSockets Secure) for localhost,
// otherwise you will get the 'loadingOverlay' stay and stays on screen with ERROR:
// "WebSocket disconnected. Attempting to reconnect..." blocking the webpage http://localhost:7222
WS_URL = `ws://localhost:${WS_PORT}/ws`; // localhost WS (unsecured)
//alert(`Running locally with WS_URL=${WS_URL}`);
}
2024-10-21 11:03:33 +02:00
let socket;
let reconnectAttempts = 0;
const maxReconnectAttempts = 5;
const reconnectInterval = 5000; // 5 seconds
function connectWebSocket() {
console.log("Attempting to connect to WebSocket...");
document.getElementById('loadingMessage').textContent = 'Connecting to WebSocket...';
socket = new WebSocket(WS_URL);
socket.onopen = function(e) {
console.log("Connected to WebSocket");
document.getElementById('loadingOverlay').style.display = 'none';
reconnectAttempts = 0;
initializeUI();
};
socket.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
console.log(`Data received from server:`, data);
if (data.type === 'heartbeat') {
sendWebSocketMessage('heartbeat', {});
} else if (data.type === 'model_download_progress') {
updateModelDownloadProgress(data.data);
} else if (data.type === 'status_update') {
updateAppStatus(data.data);
}
// Handle other message types as needed
} catch (error) {
console.error('Error parsing WebSocket message:', error, 'Raw message:', event.data);
2024-10-12 14:46:41 +02:00
}
2024-10-21 11:03:33 +02:00
};
2024-10-12 14:46:41 +02:00
2024-10-21 11:03:33 +02:00
socket.onclose = function(event) {
if (event.wasClean) {
console.log(`Connection closed cleanly, code=${event.code} reason=${event.reason}`);
} else {
console.log('Connection died');
2024-10-12 14:46:41 +02:00
}
2024-10-21 11:03:33 +02:00
handleReconnect();
};
socket.onerror = function(error) {
console.log(`WebSocket Error: ${error.message}`);
handleReconnect();
};
}
function handleReconnect() {
if (reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
document.getElementById('loadingOverlay').style.display = 'flex';
document.getElementById('loadingMessage').textContent = `WebSocket disconnected. Attempting to reconnect (${reconnectAttempts}/${maxReconnectAttempts})...`;
setTimeout(connectWebSocket, reconnectInterval);
} else {
document.getElementById('loadingMessage').textContent = 'Failed to connect to WebSocket. Please refresh the page or contact support.';
}
}
function sendWebSocketMessage(type, data) {
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ type, data }));
}
}
let lastUpdateTime = 0;
const updateInterval = 250; // Update every 250ms
function updateModelDownloadProgress(data) {
const currentTime = Date.now();
if (currentTime - lastUpdateTime < updateInterval) {
return; // Skip update if it's too soon
}
lastUpdateTime = currentTime;
const progressBar = document.querySelector('#model-download-progress .progress-bar-fill');
const statusDiv = document.getElementById('model-download-status');
const speedDiv = document.getElementById('model-download-speed');
const etaDiv = document.getElementById('model-download-eta');
if (data.percentage !== undefined) {
progressBar.style.width = `${data.percentage}%`;
progressBar.textContent = `${data.percentage.toFixed(2)}%`;
}
statusDiv.textContent = data.message || '';
if (data.speed) {
speedDiv.textContent = `Speed: ${data.speed}`;
} else {
speedDiv.textContent = 'Speed: N/A';
}
if (data.eta) {
etaDiv.textContent = `ETA: ${formatTime(data.eta)}`;
} else {
etaDiv.textContent = 'ETA: Calculating...';
}
if (data.stage === 'Complete') {
// lutzapps - clear 'Speed: N/A' and 'ETA: Calculating...' Div textContent, when no eta/speed data, but 'Complete'
etaDiv.textContent = '';
speedDiv.textContent = '';
2024-10-21 11:03:33 +02:00
loadModelFolders(); // Refresh the Existing Models section when download is complete
}
}
function updateAppStatus(appKey, status) {
appStatuses[appKey] = status;
const statusElement = document.getElementById(`status-${appKey}`);
if (statusElement) {
statusElement.textContent = status.charAt(0).toUpperCase() + status.slice(1);
statusElement.className = `status status-${status}`;
document.getElementById(`start-${appKey}`).disabled = (status === 'running');
document.getElementById(`stop-${appKey}`).disabled = (status === 'stopped');
document.getElementById(`open-${appKey}`).disabled = (status === 'stopped');
}
}
async function startApp(appKey) {
const response = await fetch(`/start/${appKey}`);
const data = await response.json();
if (data.status === 'started') {
updateAppStatus(appKey, 'running');
viewLogs(appKey, appConfigs[appKey].name);
} else if (data.status === 'error') {
alert(data.message);
}
}
async function stopApp(appKey) {
const response = await fetch(`/stop/${appKey}`);
const data = await response.json();
if (data.status === 'stopped') {
updateAppStatus(appKey, 'stopped');
} else if (data.status === 'error') {
alert(data.message);
}
await updateStatus();
}
function openApp(appKey, port) {
// *** lutzapps - Change #3 - support to run locally
// NOTE: ` (back-ticks) are used here for template literals
var url = `https://${podId}-${port}.proxy.runpod.net/`; // need to be declared as var
if (`${enable_unsecure_localhost}` === 'True') {
url = `http://localhost:${port}/`; // remove runpod.net proxy
//alert(`openApp URL=${url}`);
}
2024-10-21 11:03:33 +02:00
window.open(url, '_blank');
}
async function updateStatus() {
const response = await fetch('/status');
const data = await response.json();
for (const [appKey, status] of Object.entries(data)) {
updateAppStatus(appKey, status);
}
}
function viewLogs(appKey, appName) {
currentLogApp = appKey;
currentLogAppName = appName;
document.getElementById('currentAppName').textContent = `Logs: ${appName}`;
updateLogs();
document.getElementById('downloadLogsBtn').style.display = 'flex';
}
function updateLogs() {
if (currentLogApp) {
fetch(`/logs/${currentLogApp}`)
.then(response => response.json())
.then(data => {
const logsDiv = document.getElementById('logs');
const wasScrolledToBottom = logsDiv.scrollHeight - logsDiv.clientHeight <= logsDiv.scrollTop + 1;
logsDiv.textContent = data.logs.join('\n');
if (wasScrolledToBottom) {
logsDiv.scrollTop = logsDiv.scrollHeight;
}
document.getElementById('downloadLogsBtn').style.display = 'flex';
});
} else {
document.getElementById('downloadLogsBtn').style.display = 'none';
2024-10-12 14:46:41 +02:00
}
2024-10-21 11:03:33 +02:00
}
2024-10-12 14:46:41 +02:00
2024-10-21 11:03:33 +02:00
async function forceKillApp(appKey) {
if (confirm(`Are you sure you want to force kill ${appKey}? This may cause data loss.`)) {
try {
const response = await fetch(`/force_kill/${appKey}`, { method: 'POST' });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.status === 'killed') {
updateAppStatus(appKey, 'stopped');
alert(data.message);
} else {
alert(`Error: ${data.message}`);
}
} catch (error) {
console.error('Error:', error);
alert(`Error killing ${appKey}: ${error.message}`);
2024-10-12 14:46:41 +02:00
}
await updateStatus();
}
2024-10-21 11:03:33 +02:00
}
2024-10-12 14:46:41 +02:00
2024-10-21 11:03:33 +02:00
function initializeUI() {
updateStatus();
setInterval(updateStatus, 5000);
setInterval(updateLogs, 1000);
var data = {};
data.cmd = 'refreshModelTypes';
extendUIHelper(data); // lutzapps - initialize the available SHARED_MODEL_FOLDERS for the "Model Downloader" modelType select list
}
// lutzapps - populate modeltype select options from shared_models
async function extendUIHelper(data) {
// check the data
// if no data is passed, the default cmd is "selectModelType" with no specific "model_type",
// which means to re-select the last selected modelType option from the global var MODELTYPE_SELECTED
var cmd = "selectModelType"; // default cmd, when called with empty data or empty cmd
var model_type = MODELTYPE_SELECTED; // the 'modelType' option value (=foldername) for the select dropdown list
var token = ""; // the token value of HF_TOKEN or CIVITAI_API_TOKEN to pass to the corresponding WebUI Password fields on the "Models" tab
var response;
var result;
if (data !== undefined && data.cmd !== undefined) {
cmd = data.cmd;
if (cmd === "selectModelType" && data.model_type !== undefined) {
model_type = data.model_type; // the model_type which is passed in to select
}
if ((cmd === "hfToken" || cmd === "civitaiToken") // cmd need to match the DOM id of the Password field on the "Models" tab
&& data.token !== undefined) {
token = data.token; // if token = undefined or empty "", then the corresponding token get fetched from the server
}
}
//alert("extendUIHelper(): cmd=" + cmd +", model_type=" + model_type + ", token=" + token); // debug-info (DISABLED)
switch (cmd) {
case "civitaiToken":
if (token === "") { // get the data from the Server
response = await fetch('/get_civitai_token');
result = await response.json();
token = result['token'];
}
alert(cmd + "=" + token);
// pass tokens from HF or CIVITAI ENV vars into their Password fields
document.getElementById(cmd).value = token; //'********'; // indicate a found token, but require to call back
break;
case "hfToken":
if (token === "") { // get the data from the Server
response = await fetch('/get_huggingface_token');
result = await response.json();
token = result['token'];
}
alert(cmd + "=" + token);
// pass tokens from HF or CIVITAI ENV vars into their Password fields
document.getElementById(cmd).value = token; //'********'; // indicate a found token, but require to call back
break;
case "refreshModelTypes":
// refresh and optionally select the 'modelType' list for "Model Downloader"
var modelTypeSelect = document.getElementById('modelType');
// get the data from the Server
response = await fetch('/get_model_types');
result = await response.json();
//alert(JSON.stringify(result)); // show the JSON-String
var model_types = result; // get the JSON-Object
var count = Object.keys(model_types).length; // count=18, when using the default SHARED_MODEL_FOLDERS dict
// the "/get_model_types" app.get_model_types_route() function checks
// if the SHARED_MODELS_DIR shared files already exists at the "/workspace" location.
// that only happens AFTER the the user clicked the "Create Shared Folders" button
// on the "Settings" Tab of the app's WebUI.
// it will return an empty model_types_dict, so the "Download Manager" does NOT get
// the already in-memory SHARED_MODEL_FOLDERS code-generated default dict
// BEFORE the workspace folders in SHARED_MODELS_DIR exists!
//
// when SHARED_MODELS_DIR exists (or updates), this function will be called via a Socket Message
// to "refresh" its content automatically
var modelTypeSelected = modelTypeSelect.value; // remember the current selected modelType.option value
modelTypeSelect.options.length = 0; // clear all current modelTypeSelect options
for (i = 0 ; i < count; i += 1) {
modelTypeOption = document.createElement('option');
modelType = model_types[String(i)]['modelfolder'];
modelTypeOption.setAttribute('value', modelType);
modelTypeOption.appendChild(document.createTextNode(model_types[String(i)]['desc']));
//if (modelFolder === modelTypeSelected) {
// modelTypeOption.selected = true; // reselect it
//}
modelTypeSelect.appendChild(modelTypeOption);
}
//modelTypeSelect.selectedIndex = modelfolder_index; // set the selected index
//modelTypeSelect.options[mmodelfolder_index].selected = true; // and mark it as "selected" option
if (modelTypeSelected === "") { // initial refresh, called by initializeUI() function
modelTypeSelect.selectedIndex = 0; // use the first modelType option, usually "ckpt"
MODELTYPE_SELECTED = modelTypeSelect.options[0].value; // NOT handled by the onchange() event handler
}
else {
modelTypeSelect.value = modelTypeSelected; // (re-)apply the selected modelType option
MODELTYPE_SELECTED = modelTypeSelected; // NOT handled by the onchange() event handler
}
break;
case "selectModelType":
// this is called by model_utils:download_civitai_model() which passed the downloading 'model_type'
// to select for clarity, which can be different from the currently selected modelType option
// if called without a 'model_type', the last MODELTYPE_SELECTED will be selected (after a "Tab Switch")
// refresh and optionally select the 'modelType' list for "Model Downloader"
var modelTypeSelect = document.getElementById('modelType');
modelTypeSelect.value = model_type;
MODELTYPE_SELECTED = model_type; // NOT handled by the onchange() event handler
break;
default: // no cmd passed is same as "selectModelType" without a 'model_type'
// this is already handled by "selectModelType" defaults, there is no "default" case needed here
}
2024-10-21 11:03:33 +02:00
}
async function downloadModel() {
const url = document.getElementById('modelUrl').value;
const modelName = document.getElementById('modelName').value;
const modelType = document.getElementById('modelType').value;
//const hfToken = document.getElementById('hfToken').value;
let hfToken = null;
let civitaiToken = null;
// lutzapps - support HF_TOKEN ENV var
// Check if the URL is from Huggingface
if (url.toLowerCase().includes('huggingface.co')) { // be case-insensitive with this url
hfToken = document.getElementById('hfToken').value;
if (hfToken === '********') {
// if the token is masked, fetch it from the server
const response = await fetch('/get_huggingface_token');
const result = await response.json();
hfToken = result.token;
}
}
// Check if the URL is from Civitai
if (url.toLowerCase().includes('civitai.com')) { // lutzapps - be case-insensitive with this url
civitaiToken = document.getElementById('civitaiToken').value;
if (civitaiToken === '********') {
// If the token is masked, fetch it from the server
const response = await fetch('/get_civitai_token');
const result = await response.json();
civitaiToken = result.token;
}
if (!civitaiToken) {
alert('Civitai API token is required for Civitai URLs. Please enter it in the Civitai API Token field.');
return;
}
2024-10-21 11:03:33 +02:00
}
await startModelDownload(url, modelName, modelType, civitaiToken, hfToken);
2024-10-21 11:03:33 +02:00
}
async function startModelDownload(url, modelName, modelType, civitaiToken, hfToken, versionId = null, fileIndex = null) {
const progressBar = document.querySelector('#model-download-progress .progress-bar-fill');
const statusDiv = document.getElementById('model-download-status');
const speedDiv = document.getElementById('model-download-speed');
const etaDiv = document.getElementById('model-download-eta');
document.getElementById('model-download-progress').style.display = 'block';
progressBar.style.width = '0%';
progressBar.textContent = '0%';
statusDiv.textContent = 'Starting download...';
speedDiv.textContent = '';
etaDiv.textContent = '';
try {
const response = await fetch('/download_model', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
url,
model_name: modelName,
model_type: modelType,
civitai_token: civitaiToken,
hf_token: hfToken,
version_id: versionId,
file_index: fileIndex
}),
});
2024-10-12 14:46:41 +02:00
2024-10-21 11:03:33 +02:00
const result = await response.json();
if (result.status === 'success') {
statusDiv.textContent = result.message;
progressBar.style.width = '100%';
progressBar.textContent = '100%';
await loadModelFolders();
showRecreateSymlinksButton();
} else if (result.status === 'choice_required') {
if (result.data.type === 'file') {
showFileChoiceDialog(result.data, url, modelName, modelType, civitaiToken, hfToken);
} else if (result.data.type === 'version') {
showVersionChoiceDialog(result.data, url, modelName, modelType, civitaiToken, hfToken);
}
} else {
statusDiv.textContent = `Error: ${result.message}`;
}
} catch (error) {
console.error('Error:', error);
statusDiv.textContent = `Error: ${error.message}`;
}
}
function showVersionChoiceDialog(data, url, modelName, modelType, civitaiToken, hfToken) {
const dialogContent = `
<h3>Multiple versions available. Please choose one:</h3>
<ul>
${data.versions.map((version, index) => `
<li>
<button onclick="selectVersion(${version.id}, '${url}', '${modelName}', '${modelType}', '${civitaiToken}', '${hfToken}')">
${version.name} (Created: ${new Date(version.createdAt).toLocaleString()})
</button>
</li>
`).join('')}
</ul>
`;
showDialog(dialogContent);
}
function showFileChoiceDialog(data, url, modelName, modelType, civitaiToken, hfToken) {
const modalContent = `
<div class="modal-content">
<span class="close">&times;</span>
<h3>Multiple files available. Please choose one:</h3>
<ul>
${data.files.map((file, index) => `
<li>
<button onclick="selectFile(${index}, '${url}', '${modelName}', '${modelType}', '${civitaiToken}', '${hfToken}', ${data.version_id})">
${file.name} (${formatSize(file.size * 1024)}, Type: ${file.type})
</button>
</li>
`).join('')}
</ul>
</div>
`;
const modal = document.createElement('div');
modal.className = 'modal';
modal.innerHTML = modalContent;
document.body.appendChild(modal);
modal.style.display = 'block';
const closeBtn = modal.querySelector('.close');
closeBtn.onclick = function() {
document.body.removeChild(modal);
}
window.onclick = function(event) {
if (event.target == modal) {
document.body.removeChild(modal);
2024-10-12 14:46:41 +02:00
}
}
2024-10-21 11:03:33 +02:00
}
function selectVersion(versionId, url, modelName, modelType, civitaiToken, hfToken) {
document.body.removeChild(document.body.lastChild); // Remove the dialog
startModelDownload(url, modelName, modelType, civitaiToken, hfToken, versionId);
}
function selectFile(fileIndex, url, modelName, modelType, civitaiToken, hfToken, versionId) {
const modal = document.querySelector('.modal');
document.body.removeChild(modal);
startModelDownload(url, modelName, modelType, civitaiToken, hfToken, versionId, fileIndex);
}
async function loadModelFolders() {
try {
const response = await fetch('/get_model_folders');
const folders = await response.json();
const modelFoldersDiv = document.getElementById('model-folders');
modelFoldersDiv.innerHTML = '';
for (const [folderName, folderInfo] of Object.entries(folders)) {
const folderDiv = document.createElement('div');
folderDiv.className = 'model-folder';
folderDiv.innerHTML = `
<h4>${folderName}</h4>
<p>Size: ${folderInfo.size}</p>
<p>Files: ${folderInfo.file_count}</p>
`;
modelFoldersDiv.appendChild(folderDiv);
}
} catch (error) {
console.error('Error loading model folders:', error);
}
}
function showRecreateSymlinksButton() {
const buttonContainer = document.getElementById('recreate-symlinks-container');
buttonContainer.innerHTML = `
<button onclick="recreateSymlinks()" class="settings-button">Recreate Symlinks</button>
<p id="symlink-status"></p>
`;
buttonContainer.style.display = 'block';
}
async function recreateSymlinks() {
const statusElement = document.getElementById('symlink-status');
statusElement.textContent = 'Recreating symlinks...';
try {
const response = await fetch('/recreate_symlinks', { method: 'POST' });
const data = await response.json();
if (data.status === 'success') {
statusElement.textContent = data.message;
2024-10-12 14:46:41 +02:00
} else {
2024-10-21 11:03:33 +02:00
statusElement.textContent = 'Error: ' + data.message;
2024-10-12 14:46:41 +02:00
}
2024-10-21 11:03:33 +02:00
} catch (error) {
statusElement.textContent = 'Error: ' + error.message;
}
}
async function saveCivitaiToken() {
const token = document.getElementById('civitaiTokenSave').value;
try {
const response = await fetch('/save_civitai_token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ token: token }),
});
const result = await response.json();
if (result.status === 'success') {
document.getElementById('civitaiTokenStatus').textContent = 'Token saved successfully.';
document.getElementById('civitaiToken').value = '********';
document.getElementById('civitaiTokenSave').value = '********';
} else {
document.getElementById('civitaiTokenStatus').textContent = `Error: ${result.message}`;
2024-10-12 14:46:41 +02:00
}
2024-10-21 11:03:33 +02:00
} catch (error) {
console.error('Error:', error);
document.getElementById('civitaiTokenStatus').textContent = `Error: ${error.message}`;
}
}
async function loadCivitaiToken() {
try {
const response = await fetch('/get_civitai_token');
const result = await response.json();
if (result.token) {
document.getElementById('civitaiToken').value = '********';
document.getElementById('civitaiTokenSave').value = '********';
2024-10-12 14:46:41 +02:00
}
2024-10-21 11:03:33 +02:00
} catch (error) {
console.error('Error loading Civitai token:', error);
}
}
// lutzapps - added HF_TOKEN support
async function loadHFToken() {
try {
const response = await fetch('/get_huggingface_token');
const result = await response.json();
if (result.token) {
document.getElementById('hfToken').value = '********';
}
} catch (error) {
console.error('Error loading Huggingface token:', error);
}
}
2024-10-21 11:03:33 +02:00
function formatSize(sizeInBytes) {
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let size = sizeInBytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(2)} ${units[unitIndex]}`;
}
function formatTime(seconds) {
if (isNaN(seconds) || seconds < 0) {
return 'Calculating...';
}
if (seconds < 60) {
return `${seconds} seconds`;
} else if (seconds < 3600) {
return `${Math.floor(seconds / 60)} minutes ${seconds % 60} seconds`;
} else {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return `${hours} hours ${minutes} minutes`;
}
}
// Example URLs - lutzapps - TODO: enrich data
2024-10-21 11:03:33 +02:00
const exampleUrls = {
'Stable-diffusion': 'https://civitai.com/models/90352/dreamshaper',
'Lora': 'https://civitai.com/models/58390?modelVersionId=62833',
'VAE': 'https://huggingface.co/stabilityai/sd-vae-ft-mse-original/blob/main/vae-ft-mse-840000-ema-pruned.safetensors',
'ESRGAN': 'https://civitai.com/models/116225?modelVersionId=125843',
'Flux-Dev': 'https://civitai.com/models/618692?modelVersionId=691639',
'Flux-Schnell': 'https://civitai.com/models/618692?modelVersionId=699279'
};
function updateExampleUrls() {
document.getElementById('example-sd').textContent = exampleUrls['Stable-diffusion'];
document.getElementById('example-lora').textContent = exampleUrls['Lora'];
document.getElementById('example-vae').textContent = exampleUrls['VAE'];
document.getElementById('example-upscaler').textContent = exampleUrls['ESRGAN'];
document.getElementById('example-flux-dev').textContent = exampleUrls['Flux-Dev'];
document.getElementById('example-flux-schnell').textContent = exampleUrls['Flux-Schnell'];
}
// lutzapps - replace function copyToClipboard() with function useInModelDownloader()
function useInModelDownloader(modelType, modelUrl) {
// copy the downloadUrl in the Model Downloader "Url" textbox
document.getElementById('modelUrl').value = modelUrl;
// select the modelType in the modelType select list, we are just about to download
var data = {};
data.cmd = 'selectModelType';
data.model_type = modelType;
extendUIHelper(data);
navigator.clipboard.writeText(modelUrl).then(() => {
alert('URL copied to Downloader and into the Clipboard!');
}, (err) => {
console.error('Could not copy text: ', err);
});
}
// lutzapps - obsolete function (can be deleted)
2024-10-21 11:03:33 +02:00
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
alert('URL copied to clipboard!');
}, (err) => {
console.error('Could not copy text: ', err);
});
}
// Call this function when the Models tab is opened
document.querySelector('.navbar-tabs a[onclick="openTab(event, \'models-tab\')"]').addEventListener('click', function() {
//alert("querySelector");
loadModelFolders(); // lutzapps - this ModelFolders is NOT for the 'modelType' "select dropdown" model list
extendUIHelper(); // lutzapps - select the last know MODELTYPE_SELECTED in the WebUI Dom Id 'modelType' "select dropdown" model list
2024-10-21 11:03:33 +02:00
loadCivitaiToken();
loadHFToken(); // lutzapps - added HF_TOKEN ENV var Support
2024-10-21 11:03:33 +02:00
updateExampleUrls();
});
// Initialize WebSocket connection
connectWebSocket();
// ... (keep any remaining code)
function openTab(evt, tabName) {
var i, tabContent, tabLinks;
tabContent = document.getElementsByClassName("tab-content");
for (i = 0; i < tabContent.length; i++) {
tabContent[i].style.display = "none";
}
tabLinks = document.getElementsByClassName("tab-link");
for (i = 0; i < tabLinks.length; i++) {
tabLinks[i].className = tabLinks[i].className.replace(" active", "");
}
document.getElementById(tabName).style.display = "block";
evt.currentTarget.className += " active";
// Additional actions when switching tabs
if (tabName === 'apps-tab') {
document.querySelector('.logs-section').style.display = 'flex';
updateLogs(); // Refresh logs when switching back to the "Apps" tab
2024-10-21 11:03:33 +02:00
} else if (tabName === 'models-tab') {
// lutzapps - the following event handler functions already fired
// in the queryselector eventhandler (esspecially for the "models-tab" tab)
//loadModelFolders();
//loadCivitaiToken();
//loadHFToken(); // lutzapps - added HF_TOKEN Support
2024-10-21 11:03:33 +02:00
} else if (tabName === 'settings-tab') {
loadSshDetails();
updateFileBrowserStatus();
}
}
function loadSshDetails() {
const ip = '{{ RUNPOD_PUBLIC_IP }}';
const port = '{{ RUNPOD_TCP_PORT_22 }}';
const sshCommand = `ssh root@${ip} -p ${port}`;
document.getElementById('sshIp').textContent = ip;
document.getElementById('sshPort').textContent = port;
document.getElementById('sshCommand').textContent = sshCommand;
const newSshPassword = document.getElementById('newSshPassword');
const passwordButtons = document.querySelector('.password-buttons');
if ('{{ ssh_password_status }}' === 'set') {
newSshPassword.placeholder = "Password is set (hidden)";
passwordButtons.style.display = 'flex';
} else {
newSshPassword.placeholder = "Enter new SSH password";
passwordButtons.style.display = 'flex';
}
}
// Add this at the end of your script section
document.addEventListener('DOMContentLoaded', function() {
loadSshDetails();
// ... any other initialization code ...
});
function copySshCommand() {
const sshCommand = document.getElementById('sshCommand').textContent;
navigator.clipboard.writeText(sshCommand).then(() => {
alert('SSH command copied to clipboard!');
}, (err) => {
console.error('Could not copy text: ', err);
alert('Failed to copy SSH command. Please copy it manually.');
});
}
2024-10-12 14:46:41 +02:00
2024-10-21 11:03:33 +02:00
function togglePasswordVisibility() {
const passwordInput = document.getElementById('newSshPassword');
if (passwordInput.type === 'password') {
passwordInput.type = 'text';
if (passwordInput.placeholder === "Password is set (hidden)") {
passwordInput.value = "{{ ssh_password }}";
}
} else {
passwordInput.type = 'password';
if (passwordInput.value === "{{ ssh_password }}") {
passwordInput.value = '';
passwordInput.placeholder = "Password is set (hidden)";
}
2024-10-12 14:46:41 +02:00
}
}
2024-10-21 11:03:33 +02:00
async function setCustomSshPassword() {
const newPassword = document.getElementById('newSshPassword').value;
if (!newPassword) {
alert('Please enter a password.');
return;
}
if (confirm('Warning: Password-based SSH authentication is less secure than key-based authentication. Are you sure you want to set a password?')) {
2024-10-12 14:46:41 +02:00
try {
2024-10-21 11:03:33 +02:00
const response = await fetch('/set_ssh_password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ password: newPassword }),
});
const result = await response.json();
if (result.status === 'success') {
alert('SSH password set successfully.');
document.getElementById('newSshPassword').value = '';
document.getElementById('newSshPassword').placeholder = 'Password is set (hidden)';
2024-10-12 14:46:41 +02:00
} else {
2024-10-21 11:03:33 +02:00
alert('Failed to set SSH password: ' + result.message);
2024-10-12 14:46:41 +02:00
}
} catch (error) {
2024-10-21 11:03:33 +02:00
console.error('Error setting SSH password:', error);
alert('An error occurred while setting the SSH password.');
2024-10-12 14:46:41 +02:00
}
}
}
2024-10-21 11:03:33 +02:00
function openFileBrowser() {
const podId = '{{ pod_id }}';
// *** lutzapps - Change #5 - support to run locally
// NOTE: ` (back-ticks) are used here for template literals
var url = `https://${podId}-7222.proxy.runpod.net/fileapp/`; // need to be declared as var
if (`${enable_unsecure_localhost}` === 'True') {
url = `http://localhost:7222/fileapp/`; // remove runpod.net proxy
//alert(`FileBrowser URL=${url}`);
2024-10-12 14:46:41 +02:00
}
window.open(url, '_blank');
}
2024-10-21 11:03:33 +02:00
async function controlFileBrowser(action) {
try {
const response = await fetch(`/${action}_filebrowser`);
const result = await response.json();
if (result.status === 'started' || result.status === 'stopped') {
updateFileBrowserStatus();
2024-10-12 14:46:41 +02:00
}
2024-10-21 11:03:33 +02:00
} catch (error) {
console.error('Error controlling File Browser:', error);
}
2024-10-12 14:46:41 +02:00
}
2024-10-21 11:03:33 +02:00
async function updateFileBrowserStatus() {
try {
const response = await fetch('/filebrowser_status');
const result = await response.json();
const statusElement = document.getElementById('filebrowser-status');
if (statusElement) {
statusElement.textContent = result.status;
2024-10-12 14:46:41 +02:00
}
2024-10-21 11:03:33 +02:00
const startButton = document.getElementById('start-filebrowser');
const stopButton = document.getElementById('stop-filebrowser');
if (startButton && stopButton) {
startButton.disabled = (result.status === 'running');
stopButton.disabled = (result.status === 'stopped');
}
} catch (error) {
console.error('Error updating File Browser status:', error);
2024-10-12 14:46:41 +02:00
}
}
2024-10-21 11:03:33 +02:00
// Call this function periodically to update the status
setInterval(updateFileBrowserStatus, 5000);
2024-10-12 14:46:41 +02:00
2024-10-21 11:03:33 +02:00
// Update the DOMContentLoaded event listener
document.addEventListener('DOMContentLoaded', function() {
updateFileBrowserStatus();
// ... (other initialization code)
});
// Call this function when the Settings tab is opened
document.querySelector('.navbar-tabs a[onclick="openTab(event, \'settings-tab\')"]').addEventListener('click', function() {
loadSshDetails();
updateFileBrowserStatus();
});
async function installApp(appKey) {
2024-10-12 14:46:41 +02:00
const installButton = document.getElementById(`install-${appKey}`);
const progressContainer = document.getElementById(`install-progress-${appKey}`);
const logsContainer = document.getElementById(`install-logs-${appKey}`);
installButton.disabled = true;
progressContainer.style.display = 'block';
logsContainer.style.display = 'block';
logsContainer.textContent = '';
try {
const response = await fetch(`/install/${appKey}`, { method: 'POST' });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const contentType = response.headers.get("content-type");
if (contentType && contentType.indexOf("application/json") !== -1) {
const data = await response.json();
if (data.status !== 'success') {
throw new Error(data.message);
}
} else {
throw new Error("Received non-JSON response from server");
}
// Don't reload the page here, wait for the WebSocket 'install_complete' message
} catch (error) {
console.error('Installation error:', error);
appendToInstallLogs({app_name: appKey, log: `Error: ${error.message}`});
}
2024-10-21 11:03:33 +02:00
// Don't re-enable the button here, it will be handled by the WebSocket messages
}
// Update the WebSocket message handler
socket.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
console.log("WebSocket message received:", data);
if (data.type === 'heartbeat') {
sendWebSocketMessage('heartbeat', {});
} else if (data.type === 'install_progress') {
updateInstallProgress(data.data);
} else if (data.type === 'install_log') {
appendToInstallLogs(data.data);
} else if (data.type === 'model_download_progress') {
updateModelDownloadProgress(data.data);
} else if (data.type === 'status_update') {
updateAppStatus(data.data);
// lutzapps - use the extendUIHelper to "bridge" certain extensions between Python and JavaScript
} else if (data.type === 'extend_ui_helper') {
extendUIHelper(data.data);
2024-10-21 11:03:33 +02:00
}
// Handle other message types as needed
} catch (error) {
console.error('Error parsing WebSocket message:', error, 'Raw message:', event.data);
2024-10-12 14:46:41 +02:00
}
2024-10-21 11:03:33 +02:00
};
2024-10-12 14:46:41 +02:00
2024-10-21 11:03:33 +02:00
function updateInstallProgress(data) {
2024-10-12 14:46:41 +02:00
const progressContainer = document.getElementById(`install-progress-${data.app_name}`);
const downloadProgress = progressContainer.querySelector('.download-progress');
const unpackProgress = progressContainer.querySelector('.unpack-progress');
const speedDisplay = progressContainer.querySelector('.download-speed');
const etaDisplay = progressContainer.querySelector('.download-eta');
const stageDisplay = progressContainer.querySelector('.install-stage');
progressContainer.style.display = 'block';
if (data.stage === 'Downloading') {
downloadProgress.style.width = `${data.percentage}%`;
downloadProgress.textContent = `${data.percentage.toFixed(2)}%`;
speedDisplay.textContent = `Speed: ${data.speed}`;
etaDisplay.textContent = `ETA: ${formatTime(parseInt(data.eta))}`;
} else if (data.stage === 'Unpacking') {
unpackProgress.style.width = `${data.percentage}%`;
unpackProgress.textContent = `${data.percentage.toFixed(2)}%`;
speedDisplay.textContent = `Processed: ${data.processed} / ${data.total} files`;
etaDisplay.textContent = '';
} else if (data.stage === 'Download Complete') {
downloadProgress.style.width = '100%';
downloadProgress.textContent = '100%';
speedDisplay.textContent = '';
etaDisplay.textContent = '';
}
2024-10-21 11:03:33 +02:00
stageDisplay.textContent = `Stage: ${data.stage}`;
}
2024-10-12 14:46:41 +02:00
2024-10-21 11:03:33 +02:00
function formatTime(seconds) {
2024-10-12 14:46:41 +02:00
if (seconds < 60) {
return `${seconds} seconds`;
} else if (seconds < 3600) {
return `${Math.floor(seconds / 60)} minutes ${seconds % 60} seconds`;
} else {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return `${hours} hours ${minutes} minutes`;
}
}
function appendToInstallLogs(data) {
const logsContainer = document.getElementById(`install-logs-${data.app_name}`);
const logEntry = document.createElement('div');
logEntry.textContent = data.log;
logsContainer.appendChild(logEntry);
logsContainer.scrollTop = logsContainer.scrollHeight;
}
function handleInstallComplete(data) {
const installButton = document.getElementById(`install-${data.app_name}`);
if (data.status === 'success') {
console.log(data.message);
location.reload(); // Reload the page to reflect the new installation status
} else {
console.error(`Installation failed: ${data.message}`);
2024-10-21 11:03:33 +02:00
installButton.disabled = false; // Re-enable the button only on failure
2024-10-12 14:46:41 +02:00
}
2024-10-21 11:03:33 +02:00
}
2024-10-12 14:46:41 +02:00
2024-10-21 11:03:33 +02:00
async function fixCustomNodes(appKey) {
2024-10-12 14:46:41 +02:00
const fixButton = document.getElementById('fix-custom-nodes-' + appKey);
let logsContainer = document.getElementById('install-logs-' + appKey);
// Create logs container if it doesn't exist
if (!logsContainer) {
logsContainer = document.createElement('div');
logsContainer.id = 'install-logs-' + appKey;
logsContainer.className = 'install-logs';
fixButton.parentNode.insertBefore(logsContainer, fixButton.nextSibling);
}
fixButton.disabled = true;
logsContainer.style.display = 'block';
appendToInstallLogs({app_name: appKey, log: "Starting to fix custom nodes..."});
try {
const response = await fetch('/fix_custom_nodes/' + appKey, { method: 'POST' });
const data = await response.json();
if (data.status === 'success') {
appendToInstallLogs({app_name: appKey, log: 'Success: ' + data.message});
} else {
throw new Error(data.message);
}
} catch (error) {
console.error('Error fixing custom nodes:', error);
appendToInstallLogs({app_name: appKey, log: 'Error: ' + error.message});
} finally {
fixButton.disabled = false;
}
}
2024-10-21 11:03:33 +02:00
// Poddy animation
const secretSequence = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65];
2024-10-12 14:46:41 +02:00
let sequenceIndex = 0;
let lastKeyTime = 0;
let animationInterval;
let specialItemInterval;
let currentSpecialItem = '';
document.addEventListener('keydown', (e) => {
const currentTime = new Date().getTime();
if (currentTime - lastKeyTime > 1000) {
sequenceIndex = 0;
}
lastKeyTime = currentTime;
if (e.keyCode === secretSequence[sequenceIndex]) {
sequenceIndex++;
if (sequenceIndex === secretSequence.length) {
startPoddyAnimation();
sequenceIndex = 0;
}
} else {
sequenceIndex = 0;
}
});
function startPoddyAnimation() {
const animationContainer = document.getElementById('poddy-animation');
const poddyContainer = document.getElementById('poddy-container');
const specialItem = document.getElementById('special-item');
const audio = document.getElementById('poddy-audio');
animationContainer.style.display = 'block';
audio.play();
let animationTime = 0;
const poddyInterval = 400; // 0.4 seconds between each Poddy spawn attempt
const mushroomDuration = 1200; // 1.2 seconds for mushroom display
const snakeDuration = 6000; // 6 seconds for snake animation
let poddies = [];
const animationInterval = setInterval(() => {
animationTime += 100; // Increment by 100ms each interval
// Poddy animation
if (animationTime % poddyInterval === 0 && poddies.length < 15) {
if (Math.random() < 0.7) { // 70% chance to spawn a new Poddy
const poddy = document.createElement('div');
poddy.className = 'poddy';
poddy.style.left = `${Math.random() * 90}vw`;
poddy.style.top = `${Math.random() * 90}vh`;
poddyContainer.appendChild(poddy);
poddy.style.animation = 'dance 0.6s infinite, wiggle 0.3s infinite';
poddies.push(poddy);
}
}
// Mushroom animation
if ([5000, 11000, 18000, 35000, 41000, 47000, 53000, 72000].includes(animationTime)) {
poddies.forEach(poddy => poddyContainer.removeChild(poddy));
poddies = [];
specialItem.style.backgroundImage = "url('/static/mushroom.png')";
specialItem.style.display = 'block';
specialItem.style.left = '50%';
specialItem.style.top = '50%';
specialItem.style.transform = 'translate(-50%, -50%) scale(1)';
setTimeout(() => {
specialItem.style.transform = 'translate(-50%, -50%) scale(3)';
}, 100);
setTimeout(() => {
specialItem.style.display = 'none';
specialItem.style.transform = 'translate(-50%, -50%) scale(1)';
}, mushroomDuration);
}
// Snake animation
if (animationTime === 24000 || animationTime === 60000) {
poddies.forEach(poddy => poddyContainer.removeChild(poddy));
poddies = [];
specialItem.style.backgroundImage = "url('/static/snake.png')";
specialItem.style.display = 'block';
specialItem.style.left = '-30vw';
specialItem.style.top = '50%';
specialItem.style.transform = 'translateY(-50%)';
specialItem.style.transition = `left ${snakeDuration/1000}s linear`;
setTimeout(() => {
specialItem.style.left = '100vw';
}, 100);
setTimeout(() => {
specialItem.style.display = 'none';
specialItem.style.transition = 'none';
specialItem.style.left = '-30vw';
}, snakeDuration);
}
if (animationTime >= 73000) { // End animation after 73 seconds (1:13)
clearInterval(animationInterval);
animationContainer.style.display = 'none';
poddyContainer.innerHTML = '';
specialItem.style.display = 'none';
audio.pause();
audio.currentTime = 0;
}
}, 100);
2024-10-21 11:03:33 +02:00
}
2024-10-12 14:46:41 +02:00
document.getElementById('downloadLogsBtn').addEventListener('click', () => {
if (currentLogApp && currentLogAppName) {
downloadLogs(currentLogApp, currentLogAppName);
} else {
alert('Please select an app to view logs before downloading.');
}
});
async function downloadLogs(appKey, appName) {
try {
const response = await fetch('/logs/' + appKey);
const data = await response.json();
const logs = data.logs.join('\n');
const blob = new Blob([logs], { type: 'text/plain' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = appName + '_logs.txt';
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (error) {
console.error('Error downloading logs:', error);
alert('An error occurred while downloading the logs. Please try again.');
}
}
// Add a function to handle window resizing
function handleResize() {
const contentContainer = document.querySelector('.content-container');
const navbar = document.querySelector('.navbar');
const windowHeight = window.innerHeight;
const navbarHeight = navbar.offsetHeight;
contentContainer.style.height = `${windowHeight - navbarHeight}px`;
const appsSection = document.querySelector('.apps-section');
const logsSection = document.querySelector('.logs-section');
if (window.innerWidth <= 768) {
appsSection.style.height = `${(windowHeight - navbarHeight) / 2}px`;
logsSection.style.height = `${(windowHeight - navbarHeight) / 2}px`;
} else {
appsSection.style.height = `${windowHeight - navbarHeight}px`;
logsSection.style.height = `${windowHeight - navbarHeight}px`;
}
}
// Call handleResize on page load and window resize
window.addEventListener('load', handleResize);
window.addEventListener('resize', handleResize);
2024-10-21 11:03:33 +02:00
async function createSharedFolders() {
const statusElement = document.getElementById('shared-folders-status');
statusElement.textContent = 'Creating shared folders...';
try {
const response = await fetch('/create_shared_folders', { method: 'POST' });
const data = await response.json();
2024-10-12 14:46:41 +02:00
if (data.status === 'success') {
2024-10-21 11:03:33 +02:00
statusElement.textContent = data.message;
2024-10-12 14:46:41 +02:00
} else {
2024-10-21 11:03:33 +02:00
statusElement.textContent = 'Error: ' + data.message;
2024-10-12 14:46:41 +02:00
}
2024-10-21 11:03:33 +02:00
} catch (error) {
statusElement.textContent = 'Error: ' + error.message;
2024-10-12 14:46:41 +02:00
}
2024-10-21 11:03:33 +02:00
}
2024-10-12 14:46:41 +02:00
</script>
</body>
2024-10-21 11:03:33 +02:00
</html>