mirror of
https://github.com/kodxana/madiator-docker-runpod.git
synced 2024-12-04 23:40:13 +01:00
4275 lines
No EOL
150 KiB
HTML
4275 lines
No EOL
150 KiB
HTML
<!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: flex;
|
|
flex-direction: row;
|
|
gap: 8px;
|
|
margin-bottom: 10px;
|
|
}
|
|
button {
|
|
border: none;
|
|
color: white;
|
|
padding: 8px 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;
|
|
}
|
|
button:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
button i {
|
|
margin-right: 8px;
|
|
}
|
|
.start-button { background-color: #4CAF50; }
|
|
.stop-button { background-color: #f44336; }
|
|
.log-button { background-color: #008CBA; }
|
|
.open-button { background-color: #FF9800; }
|
|
.force-kill-button { background-color: #d9534f; }
|
|
|
|
.install-button {
|
|
width: 100%;
|
|
margin-top: 10px;
|
|
background-color: #9C27B0;
|
|
}
|
|
.install-button:hover {
|
|
background-color: #7B1FA2;
|
|
}
|
|
.log-button:hover:not(:disabled) {
|
|
background-color: #007aa3;
|
|
}
|
|
.open-button:hover:not(:disabled) {
|
|
background-color: #e68a00;
|
|
}
|
|
.install-button:hover {
|
|
background-color: #7B1FA2;
|
|
}
|
|
.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;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.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;
|
|
height: calc(100% - 50px); /* Adjust this value based on your layout */
|
|
}
|
|
#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,
|
|
.check-installation-app-button,
|
|
.refresh-installation-app-button,
|
|
.delete-installation-app-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;
|
|
}
|
|
|
|
.check-installation-app-button,
|
|
.refresh-installation-app-button,
|
|
.delete-installation-app-button,
|
|
.fix-custom-nodes-button {
|
|
background-color: #616261; /* darker background-color for Chrome */
|
|
width: 100%; /* make the buttons full width */
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.check-installation-app-button:hover,
|
|
.refresh-installation-app-button:hover,
|
|
.delete-installation-app-button:hover,
|
|
.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;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.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,
|
|
.check-installation-app-button,
|
|
.refresh-installation-app-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 {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 20px;
|
|
padding: 20px;
|
|
box-sizing: border-box;
|
|
width: 100%;
|
|
max-width: 100%;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
.settings-grid {
|
|
flex: 1 1 400px; /* Changed from just flex: 1 to better control growth */
|
|
min-width: 400px;
|
|
max-width: none; /* Remove max-width restriction */
|
|
}
|
|
|
|
.setting-group {
|
|
background-color: #2a2a2a;
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.setting-group h3 {
|
|
margin-top: 0;
|
|
margin-bottom: 15px;
|
|
color: #4CAF50;
|
|
}
|
|
|
|
.ssh-details, .ssh-password-form {
|
|
background-color: #333;
|
|
border-radius: 5px;
|
|
padding: 15px;
|
|
margin-top: 10px;
|
|
max-width: 100%;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.settings-button {
|
|
background-color: #4CAF50;
|
|
color: white;
|
|
padding: 10px 15px;
|
|
border: none;
|
|
border-radius: 5px;
|
|
cursor: pointer;
|
|
margin-right: 10px;
|
|
margin-bottom: 10px;
|
|
transition: background-color 0.3s;
|
|
}
|
|
|
|
.settings-button:hover {
|
|
background-color: #45a049;
|
|
}
|
|
|
|
#newSshPassword {
|
|
width: 100%;
|
|
padding: 10px;
|
|
margin-bottom: 10px;
|
|
box-sizing: border-box;
|
|
border: 1px solid #444;
|
|
border-radius: 5px;
|
|
background-color: #333;
|
|
color: #fff;
|
|
}
|
|
|
|
.password-buttons {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.ssh-security-notice {
|
|
font-size: 0.8em;
|
|
color: #888;
|
|
margin-top: 10px;
|
|
font-style: italic;
|
|
}
|
|
|
|
#filebrowser-status {
|
|
display: inline-block;
|
|
padding: 5px 10px;
|
|
border-radius: 15px;
|
|
font-size: 0.9em;
|
|
font-weight: bold;
|
|
margin: 10px 0;
|
|
background-color: #333;
|
|
}
|
|
|
|
/* Add these new styles for the model downloader */
|
|
.model-downloader {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
width: 100%;
|
|
max-width: none;
|
|
}
|
|
|
|
.model-downloader input,
|
|
.model-downloader select,
|
|
.model-downloader button {
|
|
padding: 12px;
|
|
font-size: 14px;
|
|
border-radius: 5px;
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.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 {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
|
|
gap: 15px;
|
|
width: 100%;
|
|
padding: 15px;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.example-url {
|
|
display: grid;
|
|
grid-template-columns: 150px 1fr;
|
|
gap: 15px;
|
|
align-items: center;
|
|
}
|
|
|
|
.example-label {
|
|
flex: 0 0 150px;
|
|
font-weight: bold;
|
|
color: #4CAF50;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.model-folders-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
gap: 20px;
|
|
width: 100%;
|
|
}
|
|
|
|
.model-folder {
|
|
background-color: #333;
|
|
border-radius: 5px;
|
|
padding: 20px;
|
|
height: fit-content;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
#model-download-progress {
|
|
margin-top: 20px;
|
|
min-height: 100px;
|
|
}
|
|
|
|
.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,*/
|
|
#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;
|
|
}
|
|
|
|
/* Update the CSS for the token saving textbox */
|
|
#civitaiTokenSave {
|
|
width: 100%;
|
|
padding: 10px;
|
|
margin-bottom: 10px;
|
|
box-sizing: border-box;
|
|
border: 1px solid #444;
|
|
border-radius: 5px;
|
|
background-color: #333;
|
|
color: #fff;
|
|
}
|
|
|
|
.example-urls {
|
|
background-color: #333;
|
|
border-radius: 5px;
|
|
padding: 15px;
|
|
}
|
|
|
|
.example-url {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.example-label {
|
|
flex: 0 0 150px;
|
|
font-weight: bold;
|
|
color: #4CAF50;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.model-folder {
|
|
background-color: #333;
|
|
border-radius: 5px;
|
|
padding: 15px;
|
|
}
|
|
|
|
#model-download-progress {
|
|
margin-top: 20px;
|
|
}
|
|
|
|
/* lutzapps - double definition
|
|
#model-download-status,
|
|
#model-download-speed,
|
|
#model-download-eta {
|
|
margin-top: 10px;
|
|
} */
|
|
|
|
#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 {
|
|
margin-top: 10px;
|
|
font-style: italic;
|
|
}
|
|
|
|
@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 {
|
|
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;
|
|
}
|
|
|
|
/* Add responsive styles */
|
|
@media screen and (max-width: 1600px) {
|
|
.settings-grid {
|
|
flex: 1 1 350px;
|
|
min-width: 350px;
|
|
}
|
|
|
|
.model-folders-grid {
|
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
}
|
|
|
|
.example-urls {
|
|
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
|
}
|
|
}
|
|
|
|
@media screen and (max-width: 1200px) {
|
|
.settings-grid {
|
|
flex: 1 1 300px;
|
|
min-width: 300px;
|
|
}
|
|
|
|
.model-folders-grid {
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
}
|
|
|
|
.example-urls {
|
|
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
|
}
|
|
}
|
|
|
|
@media screen and (max-width: 768px) {
|
|
.settings-grid {
|
|
flex: 1 1 100%;
|
|
min-width: 100%;
|
|
}
|
|
|
|
.model-folders-grid {
|
|
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
}
|
|
|
|
.example-urls {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.example-url {
|
|
grid-template-columns: 120px 1fr;
|
|
}
|
|
}
|
|
|
|
|
|
/* Make all top sections the same height */
|
|
.model-downloader-section,
|
|
.civitai-token-section,
|
|
.download-examples-section {
|
|
height: auto;
|
|
min-height: 400px; /* Consistent minimum height */
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
/* Style consistency for model downloader inputs */
|
|
.model-downloader input,
|
|
.model-downloader select {
|
|
width: 100%;
|
|
padding: 12px;
|
|
margin-bottom: 10px;
|
|
box-sizing: border-box;
|
|
border: 1px solid #444;
|
|
border-radius: 5px;
|
|
background-color: #333;
|
|
color: #fff;
|
|
font-size: 14px;
|
|
}
|
|
|
|
/* Style for the download button */
|
|
.model-downloader button,
|
|
.civitai-token-section button {
|
|
width: 100%;
|
|
padding: 12px;
|
|
background-color: #4CAF50;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 5px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
transition: background-color 0.3s;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.model-downloader button:hover,
|
|
.civitai-token-section button:hover {
|
|
background-color: #45a049;
|
|
}
|
|
|
|
/* Improve example URLs section */
|
|
.example-urls {
|
|
background-color: #333;
|
|
border-radius: 5px;
|
|
padding: 15px;
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
|
|
.example-url {
|
|
display: grid;
|
|
grid-template-columns: 120px 1fr;
|
|
gap: 10px;
|
|
align-items: center;
|
|
padding: 8px;
|
|
border-radius: 4px;
|
|
background-color: #2a2a2a;
|
|
}
|
|
|
|
/* Update model folders grid */
|
|
.model-folders-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
gap: 20px;
|
|
width: 100%;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.model-folder {
|
|
background-color: #333;
|
|
border-radius: 5px;
|
|
padding: 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
|
|
.model-folder h4 {
|
|
margin: 0;
|
|
color: #4CAF50;
|
|
}
|
|
|
|
.model-folder p {
|
|
margin: 5px 0;
|
|
color: #ccc;
|
|
}
|
|
|
|
/* Responsive adjustments */
|
|
@media screen and (max-width: 1400px) {
|
|
#models-tab .settings-container {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
}
|
|
|
|
.model-downloader-section,
|
|
.civitai-token-section {
|
|
min-height: 350px;
|
|
}
|
|
|
|
.download-examples-section {
|
|
grid-column: 1 / -1;
|
|
min-height: 300px;
|
|
}
|
|
}
|
|
|
|
@media screen and (max-width: 900px) {
|
|
#models-tab .settings-container {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.model-downloader-section,
|
|
.civitai-token-section,
|
|
.download-examples-section {
|
|
min-height: auto;
|
|
}
|
|
}
|
|
|
|
/* Add subtle hover effects */
|
|
.setting-group:hover {
|
|
transform: translateY(-2px);
|
|
transition: transform 0.2s ease;
|
|
}
|
|
|
|
.example-url:hover {
|
|
background-color: #383838;
|
|
transition: background-color 0.2s ease;
|
|
}
|
|
|
|
/* Progress bar styling */
|
|
.progress-container {
|
|
margin-top: 15px;
|
|
}
|
|
|
|
.progress-bar {
|
|
height: 20px;
|
|
background-color: #333;
|
|
border-radius: 10px;
|
|
overflow: hidden;
|
|
margin-top: 5px;
|
|
}
|
|
|
|
.progress-bar-fill {
|
|
height: 100%;
|
|
background-color: #4CAF50;
|
|
transition: width 0.3s ease;
|
|
}
|
|
|
|
/* Update the settings container layout */
|
|
.settings-container {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 20px;
|
|
padding: 20px;
|
|
box-sizing: border-box;
|
|
width: 100%;
|
|
max-width: 100%;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
.settings-grid {
|
|
flex: 1 1 400px; /* Changed from just flex: 1 to better control growth */
|
|
min-width: 400px;
|
|
max-width: none; /* Remove max-width restriction */
|
|
}
|
|
|
|
.setting-group {
|
|
background-color: #2a2a2a;
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
/* Update model folders grid for better responsiveness */
|
|
.model-folders-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
gap: 20px;
|
|
width: 100%;
|
|
}
|
|
|
|
/* Add responsive breakpoints */
|
|
@media screen and (max-width: 1600px) {
|
|
.settings-grid {
|
|
flex: 1 1 350px;
|
|
min-width: 350px;
|
|
}
|
|
|
|
.model-folders-grid {
|
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
}
|
|
|
|
.example-urls {
|
|
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
|
}
|
|
}
|
|
|
|
@media screen and (max-width: 1200px) {
|
|
.settings-grid {
|
|
flex: 1 1 300px;
|
|
min-width: 300px;
|
|
}
|
|
|
|
.model-folders-grid {
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
}
|
|
|
|
.example-urls {
|
|
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
|
}
|
|
}
|
|
|
|
@media screen and (max-width: 768px) {
|
|
.settings-grid {
|
|
flex: 1 1 100%;
|
|
min-width: 100%;
|
|
}
|
|
|
|
.model-folders-grid {
|
|
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
}
|
|
|
|
.example-urls {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.example-url {
|
|
grid-template-columns: 120px 1fr;
|
|
}
|
|
}
|
|
|
|
/* Add horizontal scrolling prevention */
|
|
body {
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
.content-container {
|
|
max-width: 100vw;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
/* Ensure proper tab content sizing */
|
|
.tab-content {
|
|
width: 100%;
|
|
max-width: 100%;
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
|
|
/* Update example URLs section */
|
|
.example-urls {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
|
|
gap: 15px;
|
|
width: 100%;
|
|
padding: 15px;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.example-url {
|
|
display: grid;
|
|
grid-template-columns: 150px 1fr;
|
|
gap: 15px;
|
|
align-items: center;
|
|
}
|
|
|
|
.example-link {
|
|
word-break: break-all;
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
/* Update the models tab container layout */
|
|
#models-tab .settings-container {
|
|
display: grid;
|
|
grid-template-columns: 350px 1fr;
|
|
gap: 20px;
|
|
padding: 20px;
|
|
box-sizing: border-box;
|
|
width: 100%;
|
|
height: calc(100vh - 140px);
|
|
}
|
|
|
|
/* Right column with tabs */
|
|
.right-column {
|
|
background-color: #2a2a2a;
|
|
border-radius: 10px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Inner tabs navigation */
|
|
.inner-tabs {
|
|
display: flex;
|
|
background-color: #333;
|
|
padding: 10px 10px 0;
|
|
gap: 5px;
|
|
}
|
|
|
|
.inner-tab-link {
|
|
padding: 10px 20px;
|
|
background-color: #2a2a2a;
|
|
border: none;
|
|
border-radius: 5px 5px 0 0;
|
|
color: #888;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.inner-tab-link.active {
|
|
background-color: #4CAF50;
|
|
color: white;
|
|
}
|
|
|
|
/* Inner tab content */
|
|
.inner-tab-content {
|
|
display: none;
|
|
padding: 20px;
|
|
overflow-y: auto;
|
|
height: 100%;
|
|
}
|
|
|
|
.inner-tab-content.active {
|
|
display: block;
|
|
}
|
|
|
|
/* Settings page grid layout */
|
|
#settings-tab .settings-container {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
|
gap: 20px;
|
|
padding: 20px;
|
|
max-width: 1800px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
/* Settings cards */
|
|
#settings-tab .setting-group {
|
|
background-color: #2a2a2a;
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
height: fit-content;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
/* Responsive adjustments */
|
|
@media screen and (max-width: 1200px) {
|
|
#models-tab .settings-container {
|
|
grid-template-columns: 1fr;
|
|
height: auto;
|
|
}
|
|
|
|
#settings-tab .settings-container {
|
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
}
|
|
}
|
|
|
|
@media screen and (max-width: 768px) {
|
|
#settings-tab .settings-container {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.inner-tabs {
|
|
padding: 5px 5px 0;
|
|
}
|
|
|
|
.inner-tab-link {
|
|
padding: 8px 15px;
|
|
font-size: 14px;
|
|
}
|
|
}
|
|
|
|
/* Model Downloader section */
|
|
.model-downloader-section {
|
|
background-color: #2a2a2a;
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
grid-column: 1;
|
|
grid-row: 1;
|
|
}
|
|
|
|
/* Civitai API Token section */
|
|
.civitai-token-section {
|
|
background-color: #2a2a2a;
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
grid-column: 1;
|
|
grid-row: 2;
|
|
}
|
|
|
|
/* Download Examples section */
|
|
.download-examples-section {
|
|
background-color: #2a2a2a;
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
grid-column: 2;
|
|
grid-row: 1;
|
|
}
|
|
|
|
/* Existing Models section */
|
|
.model-folders-container {
|
|
background-color: #2a2a2a;
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
grid-column: 2;
|
|
grid-row: 2;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
/* Example URLs styling */
|
|
.example-urls {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
gap: 10px;
|
|
width: 100%;
|
|
}
|
|
|
|
.example-url {
|
|
display: grid;
|
|
grid-template-columns: 120px 1fr;
|
|
gap: 10px;
|
|
align-items: center;
|
|
background-color: #333;
|
|
padding: 8px;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
/* Model folders grid */
|
|
.model-folders-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
gap: 15px;
|
|
width: 100%;
|
|
}
|
|
|
|
/* Scrollbar styling */
|
|
.model-folders-container::-webkit-scrollbar {
|
|
width: 8px;
|
|
}
|
|
|
|
.model-folders-container::-webkit-scrollbar-track {
|
|
background: #1a1a1a;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.model-folders-container::-webkit-scrollbar-thumb {
|
|
background: #4CAF50;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
/* Responsive adjustments */
|
|
@media screen and (max-width: 1200px) {
|
|
#models-tab .settings-container {
|
|
grid-template-columns: 300px 1fr;
|
|
}
|
|
|
|
.example-urls {
|
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
}
|
|
}
|
|
|
|
@media screen and (max-width: 900px) {
|
|
#models-tab .settings-container {
|
|
grid-template-columns: 1fr;
|
|
grid-template-rows: auto auto auto 1fr;
|
|
}
|
|
|
|
.model-downloader-section,
|
|
.civitai-token-section,
|
|
.download-examples-section,
|
|
.model-folders-container {
|
|
grid-column: 1;
|
|
}
|
|
|
|
.model-downloader-section { grid-row: 1; }
|
|
.civitai-token-section { grid-row: 2; }
|
|
.download-examples-section { grid-row: 3; }
|
|
.model-folders-container { grid-row: 4; }
|
|
}
|
|
|
|
/* Input and button styling */
|
|
.model-downloader input,
|
|
.model-downloader select,
|
|
.civitai-token-section input {
|
|
width: 100%;
|
|
padding: 10px;
|
|
margin-bottom: 10px;
|
|
border: 1px solid #444;
|
|
border-radius: 5px;
|
|
background-color: #333;
|
|
color: #fff;
|
|
}
|
|
|
|
/* Ensure proper rounded corners and shadows */
|
|
.model-downloader-section,
|
|
.civitai-token-section,
|
|
.download-examples-section,
|
|
.model-folders-container {
|
|
border-radius: 10px;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
/* Left column container */
|
|
.left-column {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
height: 100%;
|
|
}
|
|
|
|
/* Model Downloader section */
|
|
.model-downloader-section {
|
|
background-color: #2a2a2a;
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
grid-column: 1;
|
|
grid-row: 1;
|
|
}
|
|
|
|
/* Civitai API Token section */
|
|
.civitai-token-section {
|
|
background-color: #2a2a2a;
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
grid-column: 1;
|
|
grid-row: 2;
|
|
}
|
|
|
|
/* Constrain input widths */
|
|
.model-downloader {
|
|
max-width: 100%;
|
|
width: 100%;
|
|
}
|
|
|
|
/* Progress info container */
|
|
#model-download-progress {
|
|
margin-top: 20px;
|
|
padding: 15px;
|
|
background-color: #333;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
/* Existing Models section */
|
|
.model-folders-container {
|
|
background-color: #2a2a2a;
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
grid-column: 2;
|
|
grid-row: 2;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
/* Model folders grid */
|
|
.model-folders-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
gap: 15px;
|
|
width: 100%;
|
|
}
|
|
|
|
/* Style scrollbars */
|
|
.model-folders-container::-webkit-scrollbar {
|
|
width: 8px;
|
|
}
|
|
|
|
.model-folders-container::-webkit-scrollbar-track {
|
|
background: #1a1a1a;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.model-folders-container::-webkit-scrollbar-thumb {
|
|
background: #4CAF50;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
/* Responsive adjustments */
|
|
@media screen and (max-width: 1200px) {
|
|
#models-tab .settings-container {
|
|
grid-template-columns: 1fr;
|
|
height: auto;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.model-folders-container {
|
|
max-height: 600px;
|
|
}
|
|
}
|
|
|
|
/* Ensure proper rounded corners and shadows */
|
|
.model-downloader-section,
|
|
.civitai-token-section,
|
|
.model-folders-container {
|
|
border-radius: 10px;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
/* Model folder styling */
|
|
.model-folder {
|
|
background-color: #333;
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
}
|
|
|
|
/* Input and button styling */
|
|
.model-downloader input,
|
|
.model-downloader select,
|
|
.civitai-token-section input {
|
|
width: 100%;
|
|
padding: 12px;
|
|
margin-bottom: 10px;
|
|
border: 1px solid #444;
|
|
border-radius: 5px;
|
|
background-color: #333;
|
|
color: #fff;
|
|
}
|
|
|
|
.model-downloader button,
|
|
.civitai-token-section button {
|
|
width: 100%;
|
|
padding: 12px;
|
|
background-color: #4CAF50;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 5px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* Update settings page layout */
|
|
#settings-tab .settings-container {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
|
gap: 20px;
|
|
padding: 20px;
|
|
margin-bottom: 60px; /* Space for footer */
|
|
max-width: 1800px;
|
|
margin-left: auto;
|
|
margin-right: auto;
|
|
}
|
|
|
|
/* Settings grid items */
|
|
#settings-tab .settings-grid {
|
|
min-width: unset;
|
|
max-width: unset;
|
|
flex: unset;
|
|
}
|
|
|
|
/* Model folders grid */
|
|
.model-folders-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
gap: 15px;
|
|
width: 100%;
|
|
}
|
|
|
|
.model-folder {
|
|
background-color: #333;
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
height: fit-content;
|
|
}
|
|
|
|
/* Responsive adjustments */
|
|
@media screen and (max-width: 1200px) {
|
|
#models-tab .settings-container {
|
|
grid-template-columns: 1fr; /* Stack on smaller screens */
|
|
}
|
|
}
|
|
|
|
@media screen and (max-width: 768px) {
|
|
#settings-tab .settings-container {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.model-folders-grid {
|
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
|
}
|
|
}
|
|
|
|
/* Ensure proper spacing from footer */
|
|
.content-container {
|
|
padding-bottom: 60px; /* Space for footer */
|
|
}
|
|
|
|
/* Update footer positioning */
|
|
.copyright {
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
background-color: #1a1a1a;
|
|
padding: 10px;
|
|
text-align: center;
|
|
z-index: 100;
|
|
}
|
|
|
|
/* Add container padding to prevent content being cut off by footer */
|
|
.main-container {
|
|
padding-bottom: 60px;
|
|
}
|
|
|
|
/* Ensure proper rounded corners and shadows for all cards */
|
|
.setting-group,
|
|
.model-downloader-section,
|
|
.civitai-token-section,
|
|
.model-folders-container,
|
|
.model-folder {
|
|
border-radius: 10px;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Update main container to prevent scrolling */
|
|
.main-container {
|
|
height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Content container adjustments */
|
|
.content-container {
|
|
flex: 1;
|
|
overflow: hidden;
|
|
padding: 0 20px 20px 20px; /* Add padding to prevent content touching edges */
|
|
}
|
|
|
|
/* Models tab layout */
|
|
#models-tab .settings-container {
|
|
display: grid;
|
|
grid-template-columns: 350px 1fr;
|
|
gap: 20px;
|
|
height: calc(100vh - 140px); /* Adjusted height */
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Left column */
|
|
.left-column {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
height: 100%;
|
|
}
|
|
|
|
/* Model Downloader section with integrated token */
|
|
.model-downloader-section {
|
|
background-color: #2a2a2a;
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
}
|
|
|
|
/* Remove separate Civitai token section since it's now integrated */
|
|
.civitai-token-section {
|
|
display: none;
|
|
}
|
|
|
|
/* Right column */
|
|
.right-column {
|
|
background-color: #2a2a2a;
|
|
border-radius: 10px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Tab content */
|
|
.inner-tab-content {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 20px;
|
|
}
|
|
|
|
/* Ensure proper spacing from footer */
|
|
.copyright {
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
background-color: #1a1a1a;
|
|
padding: 10px;
|
|
text-align: center;
|
|
z-index: 100;
|
|
}
|
|
|
|
/* Add margin to prevent content being cut off by footer */
|
|
.settings-container {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
/* Ensure proper rounded corners */
|
|
.model-downloader-section,
|
|
.right-column,
|
|
.model-folder {
|
|
border-radius: 10px;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
/* Update scrollbar styling */
|
|
.inner-tab-content::-webkit-scrollbar {
|
|
width: 8px;
|
|
}
|
|
|
|
.inner-tab-content::-webkit-scrollbar-track {
|
|
background: #1a1a1a;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.inner-tab-content::-webkit-scrollbar-thumb {
|
|
background: #4CAF50;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
/* Responsive adjustments */
|
|
@media screen and (max-width: 1200px) {
|
|
#models-tab .settings-container {
|
|
grid-template-columns: 1fr;
|
|
height: auto;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.model-folders-container {
|
|
max-height: 600px;
|
|
}
|
|
}
|
|
|
|
/* Update main container layout */
|
|
.main-container {
|
|
height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
padding-bottom: 40px; /* Reduced padding for footer */
|
|
}
|
|
|
|
/* Content container adjustments */
|
|
.content-container {
|
|
flex: 1;
|
|
overflow: hidden;
|
|
padding: 0 20px;
|
|
}
|
|
|
|
/* Models tab layout */
|
|
#models-tab .settings-container {
|
|
display: grid;
|
|
grid-template-columns: 350px 1fr;
|
|
gap: 20px;
|
|
height: calc(100vh - 140px); /* Adjusted height */
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Model Downloader section */
|
|
.model-downloader-section {
|
|
background-color: #2a2a2a;
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
/* Group buttons together */
|
|
.model-downloader {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
|
|
.button-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
/* Adjust input and button spacing */
|
|
.model-downloader input,
|
|
.model-downloader select {
|
|
padding: 10px;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
/* Right column with models */
|
|
.right-column {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
background-color: #2a2a2a;
|
|
border-radius: 10px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Model folders grid */
|
|
.model-folders-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
gap: 15px;
|
|
padding: 20px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
/* Footer adjustments */
|
|
.copyright {
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
background-color: #1a1a1a;
|
|
padding: 8px; /* Reduced padding */
|
|
text-align: center;
|
|
z-index: 100;
|
|
height: 40px; /* Fixed height */
|
|
}
|
|
|
|
/* Progress container */
|
|
#model-download-progress {
|
|
margin-top: 15px;
|
|
padding: 15px;
|
|
background-color: #333;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
/* Status messages */
|
|
#civitaiTokenStatus {
|
|
margin: 5px 0;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
/* Main container adjustments */
|
|
.main-container {
|
|
height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
padding-bottom: 30px; /* Reduced padding for footer */
|
|
}
|
|
|
|
/* Content container */
|
|
.content-container {
|
|
flex: 1;
|
|
overflow: hidden;
|
|
padding: 10px 20px;
|
|
}
|
|
|
|
/* Models tab layout */
|
|
#models-tab .settings-container {
|
|
display: grid;
|
|
grid-template-columns: 350px 1fr;
|
|
gap: 15px;
|
|
height: calc(100vh - 120px); /* Adjusted to minimize space */
|
|
}
|
|
|
|
/* Model Downloader section */
|
|
.model-downloader-section {
|
|
background-color: #2a2a2a;
|
|
border-radius: 10px;
|
|
padding: 15px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
/* Model downloader inputs and buttons */
|
|
.model-downloader input,
|
|
.model-downloader select {
|
|
padding: 8px;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.button-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 5px;
|
|
margin-top: 5px;
|
|
}
|
|
|
|
/* Right column */
|
|
.right-column {
|
|
background-color: #2a2a2a;
|
|
border-radius: 10px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Model folders grid with balanced padding */
|
|
.model-folders-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
gap: 10px;
|
|
padding: 15px;
|
|
width: calc(100% - 30px); /* Account for padding */
|
|
margin: 0 15px; /* Add margin for balanced spacing */
|
|
}
|
|
|
|
.model-folder {
|
|
background-color: #333;
|
|
border-radius: 8px;
|
|
padding: 12px;
|
|
font-size: 0.9em; /* Slightly smaller text */
|
|
height: fit-content;
|
|
}
|
|
|
|
/* Footer adjustments */
|
|
.copyright {
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
background-color: #1a1a1a;
|
|
padding: 5px; /* Reduced padding */
|
|
text-align: center;
|
|
z-index: 100;
|
|
height: 30px; /* Reduced height */
|
|
}
|
|
|
|
/* Progress container */
|
|
#model-download-progress {
|
|
margin-top: 10px;
|
|
padding: 10px;
|
|
background-color: #333;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
/* Inner tab content */
|
|
.inner-tab-content {
|
|
padding: 15px;
|
|
overflow-y: auto; /* Prevent scrolling */
|
|
}
|
|
|
|
/* Example URLs */
|
|
.example-urls {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
gap: 10px;
|
|
padding: 0;
|
|
}
|
|
|
|
.example-url {
|
|
padding: 8px;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
/* Ensure proper spacing and alignment */
|
|
h3 {
|
|
margin: 0 0 10px 0;
|
|
}
|
|
|
|
/* Responsive adjustments */
|
|
@media screen and (max-width: 1400px) {
|
|
.model-folders-grid {
|
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
}
|
|
}
|
|
|
|
@media screen and (max-width: 1200px) {
|
|
#models-tab .settings-container {
|
|
grid-template-columns: 300px 1fr;
|
|
}
|
|
}
|
|
|
|
/* Model folders grid with no horizontal scroll */
|
|
.model-folders-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
gap: 10px;
|
|
padding: 15px;
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
/* Inner tab content */
|
|
.inner-tab-content {
|
|
padding: 15px;
|
|
overflow-y: auto;
|
|
overflow-x: hidden; /* Prevent horizontal scroll */
|
|
height: 100%;
|
|
}
|
|
|
|
/* Model folder */
|
|
.model-folder {
|
|
background-color: #333;
|
|
border-radius: 8px;
|
|
padding: 12px;
|
|
font-size: 0.9em;
|
|
word-break: break-word; /* Allow text to wrap */
|
|
}
|
|
|
|
/* Right column container */
|
|
.right-column {
|
|
background-color: #2a2a2a;
|
|
border-radius: 10px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
width: 100%;
|
|
}
|
|
|
|
/* Progress container styling */
|
|
#model-download-progress {
|
|
margin-top: 15px;
|
|
padding: 15px;
|
|
background-color: #333;
|
|
border-radius: 8px;
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
/* Progress bar */
|
|
.progress-bar {
|
|
width: 100%;
|
|
height: 20px;
|
|
background-color: #444;
|
|
border-radius: 10px;
|
|
overflow: hidden;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
/* Status messages */
|
|
#model-download-status,
|
|
#model-download-speed,
|
|
#model-download-eta {
|
|
padding: 5px 0;
|
|
font-size: 14px;
|
|
line-height: 1.4;
|
|
color: #fff;
|
|
white-space: normal; /* Allow text to wrap */
|
|
word-break: break-word;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
/* Model downloader section */
|
|
.model-downloader-section {
|
|
background-color: #2a2a2a;
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: auto; /* Allow section to grow with content */
|
|
min-height: 200px;
|
|
}
|
|
|
|
/* Button group */
|
|
.button-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
margin-top: 10px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
/* Status messages with better spacing */
|
|
#model-download-status {
|
|
padding: 5px 0;
|
|
font-size: 14px;
|
|
line-height: 1.4;
|
|
color: #fff;
|
|
white-space: normal;
|
|
word-break: break-word;
|
|
margin-bottom: 15px; /* Increased margin for better spacing */
|
|
}
|
|
|
|
#model-download-speed,
|
|
#model-download-eta {
|
|
padding: 5px 0;
|
|
font-size: 14px;
|
|
line-height: 1.4;
|
|
color: #fff;
|
|
white-space: normal;
|
|
word-break: break-word;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
/* Progress container */
|
|
#model-download-progress {
|
|
margin-top: 15px;
|
|
padding: 15px;
|
|
background-color: #333;
|
|
border-radius: 8px;
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
/* Example URLs styling */
|
|
.example-urls {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 15px;
|
|
margin-top: 20px;
|
|
padding: 15px;
|
|
background-color: #333;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.example-url {
|
|
background-color: #2a2a2a;
|
|
padding: 15px;
|
|
border-radius: 8px;
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|
}
|
|
|
|
.example-url:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.example-label {
|
|
display: block;
|
|
font-weight: bold;
|
|
color: #4CAF50;
|
|
margin-bottom: 8px;
|
|
font-size: 0.9em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.example-link {
|
|
display: block;
|
|
color: #fff;
|
|
text-decoration: none;
|
|
word-break: break-all;
|
|
font-family: monospace;
|
|
font-size: 0.9em;
|
|
padding: 8px;
|
|
background-color: #1a1a1a;
|
|
border-radius: 4px;
|
|
border: 1px solid #444;
|
|
}
|
|
|
|
.example-link:hover {
|
|
background-color: #2c2c2c;
|
|
border-color: #4CAF50;
|
|
}
|
|
|
|
/* Responsive adjustments */
|
|
@media (max-width: 1200px) {
|
|
.example-urls {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</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>
|
|
<a href="#" class="tab-link" onclick="openTab(event, 'models-tab')">Models</a>
|
|
<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">
|
|
<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]['installed'] %}
|
|
<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
|
|
</button>
|
|
<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
|
|
</button>
|
|
<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 Application
|
|
</button>
|
|
<button onclick="forceKillApp('{{ app_key }}')" id="force-kill-{{ app_key }}" class="force-kill-button">
|
|
<i class="fas fa-times-circle"></i> Force Kill
|
|
</button>
|
|
<button onclick="checkAppInstallation('{{ app_key }}')" id="check-installation-{{ app_key }}" class="check-installation-app-button">
|
|
<i class="fas fa-wrench"></i> Check Application
|
|
</button>
|
|
<button onclick="refreshAppInstallation('{{ app_key }}')" id="refresh-installation-{{ app_key }}" class="refresh-installation-app-button">
|
|
<i class="fas fa-wrench"></i> Refresh Application
|
|
</button>
|
|
<button onclick="deleteAppInstallation('{{ app_key }}', '')" id="delete-installation-{{ app_key }}" class="delete-installation-app-button">
|
|
<i class="fas fa-wrench"></i> Delete Application
|
|
</button>
|
|
</div>
|
|
{% if app_key == '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 class="status">Status: {{ app_status[app_key]['status'] }}</div>
|
|
{% else %}
|
|
<div class="install-container">
|
|
<button onclick="installApp('{{ app_key }}', '')" id="install-{{ app_key }}" class="install-button">
|
|
<i class="fas fa-download"></i> Install
|
|
</button>
|
|
<div id="install-progress-{{ app_key }}" class="install-progress" style="display: none;">
|
|
<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>
|
|
</div>
|
|
<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>
|
|
</div>
|
|
<div class="progress-container">
|
|
<div class="progress-label">Install/Refresh Progress:</div>
|
|
<div class="progress-bar">
|
|
<div class="progress-bar-fill clone-progress" style="width: 0%">0%</div>
|
|
</div>
|
|
</div>
|
|
<div class="download-info">
|
|
<span class="download-speed"></span>
|
|
<span class="download-eta"></span>
|
|
</div>
|
|
<div class="install-stage"></div>
|
|
</div>
|
|
<div id="install-logs-{{ app_key }}" class="install-logs" style="display: none;"></div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
<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>
|
|
</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">
|
|
<h3>About</h3>
|
|
<div>
|
|
Version: dev-beta-v0.9.0<br><br>
|
|
<span>Manifest Url:<a class="example-link" href="{{ app_configs_manifest_url }}" target="_blank" rel="noopener noreferrer">{{ app_configs_manifest_url }}</a></span><br>
|
|
<a class="example-link" href="/static/README.md" target="_blank" rel="noopener noreferrer">README</a><br>
|
|
<a class="example-link" href="/static/README-Development.txt" target="_blank" rel="noopener noreferrer">README Development</a><br>
|
|
<a class="example-link" href="/static/README-SHARED_MODELS.txt" target="_blank" rel="noopener noreferrer">README SHARED_MODELS</a>
|
|
</div>
|
|
</div>
|
|
<div class="setting-group">
|
|
<h3>SSH Configuration</h3>
|
|
<div class="ssh-details">
|
|
<p>IP: <span id="sshIp"></span></p>
|
|
<p>Port: <span id="sshPort"></span></p>
|
|
<p>Command: <span id="sshCommand"></span></p>
|
|
<button onclick="copySshCommand()" class="settings-button">Copy SSH Command</button>
|
|
</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>
|
|
</div>
|
|
</div>
|
|
<p class="ssh-security-notice">Note: Password-based SSH authentication is less secure than key-based authentication.</p>
|
|
</div>
|
|
|
|
<div class="setting-group">
|
|
<h3>File Browser</h3>
|
|
<button onclick="openFileBrowser()" class="settings-button">Open File Browser</button>
|
|
<!-- lutzapps - re-added default logon information -->
|
|
<p style="font-size: 16px; line-height: 1.5;">
|
|
Default credentials:<br>
|
|
<strong>Username:</strong> admin<br>
|
|
<strong>Password:</strong> admin
|
|
</p>
|
|
<p>Status: <span id="filebrowser-status"></span></p>
|
|
<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>
|
|
</div>
|
|
|
|
<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>
|
|
<div id="models-tab" class="tab-content">
|
|
<div class="settings-container">
|
|
<div class="left-column">
|
|
<!-- Model Downloader Section -->
|
|
<div class="model-downloader-section">
|
|
<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 (renames model filename)">
|
|
<select id="modelType">
|
|
</select>
|
|
<input type="password" id="civitaiToken" placeholder="Civitai API Token">
|
|
<input type="password" id="hfToken" placeholder="Hugging Face API Token (optional)">
|
|
<div class="button-group">
|
|
<button onclick="saveCivitaiToken()" class="settings-button">Save Civitai Token</button>
|
|
<button onclick="downloadModel()" class="settings-button">Download Model</button>
|
|
</div>
|
|
</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>
|
|
|
|
<!-- Civitai API Token Section -->
|
|
<div class="civitai-token-section">
|
|
<h3>Civitai API Token</h3>
|
|
<input type="password" id="civitaiTokenSave" placeholder="Enter Civitai API Token">
|
|
<button onclick="saveCivitaiToken()" class="settings-button">Save Token</button>
|
|
<p id="civitaiTokenStatus"></p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Column with Tabs -->
|
|
<div class="right-column">
|
|
<div class="inner-tabs">
|
|
<button class="inner-tab-link active" onclick="switchInnerTab(event, 'example-urls')">Example URLs</button>
|
|
<button class="inner-tab-link" onclick="switchInnerTab(event, 'existing-models')">Existing Models</button>
|
|
</div>
|
|
|
|
<div id="example-urls" class="inner-tab-content active">
|
|
<div class="example-urls">
|
|
<div class="example-url">
|
|
<span class="example-label">Stable Diffusion Model:</span>
|
|
<a href="#" class="example-link" id="example-sd" onclick="useInModelDownloader('ckpt', this.textContent); return false;"></a>
|
|
</div>
|
|
<div class="example-url">
|
|
<span class="example-label">LoRA Model:</span>
|
|
<a href="#" class="example-link" id="example-lora" onclick="useInModelDownloader('loras', this.textContent); return false;"></a>
|
|
</div>
|
|
<div class="example-url">
|
|
<span class="example-label">VAE Model:</span>
|
|
<a href="#" class="example-link" id="example-vae" onclick="useInModelDownloader('vae', this.textContent); return false;"></a>
|
|
</div>
|
|
<div class="example-url">
|
|
<span class="example-label">Upscaler Model:</span>
|
|
<a href="#" class="example-link" id="example-upscaler" onclick="useInModelDownloader('upscale_models', this.textContent); return false;"></a>
|
|
</div>
|
|
<div class="example-url">
|
|
<span class="example-label">Flux Dev Model:</span>
|
|
<a href="#" class="example-link" id="example-flux-dev" onclick="useInModelDownloader('unet', this.textContent); return false;"></a>
|
|
</div>
|
|
<div class="example-url">
|
|
<span class="example-label">Flux Schnell Model:</span>
|
|
<a href="#" class="example-link" id="example-flux-schnell" onclick="useInModelDownloader('unet', this.textContent); return false;"></a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="existing-models" class="inner-tab-content">
|
|
<div class="model-folders-grid" id="model-folders">
|
|
<!-- Model folders will be populated dynamically -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="copyright">
|
|
© 2024 RunPod Better App Manager. Created by Madiator2011 & lutzapps.
|
|
</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>
|
|
|
|
<!-- Add this near the top of the body, before other scripts -->
|
|
<script>
|
|
// Make app_configs available to JavaScript
|
|
const app_configs = {
|
|
{% for app_key, app_info in apps.items() %}
|
|
"{{ app_key }}": {
|
|
"name": "{{ app_info.name }}",
|
|
"port": {{ app_info.port }}
|
|
},
|
|
{% endfor %}
|
|
};
|
|
</script>
|
|
|
|
<!-- Rest of your JavaScript code -->
|
|
<script>
|
|
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}`);
|
|
}
|
|
|
|
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();
|
|
// Start sending heartbeats immediately after connection
|
|
startHeartbeat();
|
|
};
|
|
|
|
socket.onmessage = function(event) {
|
|
try {
|
|
const data = JSON.parse(event.data);
|
|
console.log(`Data received from server:`, data);
|
|
|
|
if (data.type === 'heartbeat') {
|
|
// Reset heartbeat timeout on receiving heartbeat response
|
|
resetHeartbeatTimeout();
|
|
} 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);
|
|
}
|
|
};
|
|
|
|
socket.onclose = function(event) {
|
|
if (event.wasClean) {
|
|
console.log(`Connection closed cleanly, code=${event.code} reason=${event.reason}`);
|
|
} else {
|
|
console.log('Connection died');
|
|
}
|
|
handleReconnect();
|
|
};
|
|
|
|
socket.onerror = function(error) {
|
|
console.log(`WebSocket Error: ${error.message}`);
|
|
handleReconnect();
|
|
};
|
|
}
|
|
|
|
let heartbeatInterval;
|
|
let heartbeatTimeout;
|
|
|
|
function startHeartbeat() {
|
|
// Clear any existing intervals
|
|
if (heartbeatInterval) {
|
|
clearInterval(heartbeatInterval);
|
|
}
|
|
|
|
// Send heartbeat every 60 seconds
|
|
heartbeatInterval = setInterval(() => {
|
|
if (socket.readyState === WebSocket.OPEN) {
|
|
socket.send(JSON.stringify({ type: 'heartbeat' }));
|
|
// Set timeout to reconnect if no response within 10 seconds
|
|
setHeartbeatTimeout();
|
|
}
|
|
}, 60000);
|
|
}
|
|
|
|
function setHeartbeatTimeout() {
|
|
if (heartbeatTimeout) {
|
|
clearTimeout(heartbeatTimeout);
|
|
}
|
|
heartbeatTimeout = setTimeout(() => {
|
|
console.log("Heartbeat timeout - attempting reconnect...");
|
|
if (socket) {
|
|
socket.close();
|
|
}
|
|
handleReconnect();
|
|
}, 65000); // Increased timeout to 65 seconds
|
|
}
|
|
|
|
function resetHeartbeatTimeout() {
|
|
if (heartbeatTimeout) {
|
|
clearTimeout(heartbeatTimeout);
|
|
}
|
|
}
|
|
|
|
// Clean up on page unload
|
|
window.addEventListener('beforeunload', () => {
|
|
if (heartbeatInterval) {
|
|
clearInterval(heartbeatInterval);
|
|
}
|
|
if (heartbeatTimeout) {
|
|
clearTimeout(heartbeatTimeout);
|
|
}
|
|
if (socket) {
|
|
socket.close();
|
|
}
|
|
});
|
|
|
|
function handleReconnect() {
|
|
if (reconnectAttempts < maxReconnectAttempts) {
|
|
reconnectAttempts++;
|
|
document.getElementById('loadingOverlay').style.display = 'flex';
|
|
document.getElementById('loadingMessage').textContent =
|
|
`WebSocket disconnected. Attempting to reconnect (${reconnectAttempts}/${maxReconnectAttempts})...`;
|
|
|
|
// Add exponential backoff
|
|
const backoffTime = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
|
|
setTimeout(connectWebSocket, backoffTime);
|
|
} else {
|
|
document.getElementById('loadingMessage').textContent =
|
|
'Failed to connect to WebSocket. Installation continues in background. Please refresh page to see status.';
|
|
}
|
|
}
|
|
|
|
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 = '';
|
|
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) {
|
|
try {
|
|
const response = await fetch(`/start/${appKey}`);
|
|
const data = await response.json();
|
|
if (data.status === 'started' || data.status === 'already_running') {
|
|
updateAppStatus({ [appKey]: 'running' });
|
|
// Get the app name from app_configs and show logs
|
|
const appName = app_configs[appKey].name;
|
|
// Switch to logs view
|
|
currentLogApp = appKey;
|
|
currentLogAppName = appName;
|
|
document.getElementById('currentAppName').textContent = `Logs - ${appName}`;
|
|
document.getElementById('downloadLogsBtn').style.display = 'flex';
|
|
document.getElementById('logs').textContent = ''; // Clear existing logs
|
|
updateLogs(); // Start updating logs
|
|
|
|
// Make sure logs section is visible
|
|
const logsSection = document.querySelector('.logs-section');
|
|
if (logsSection) {
|
|
logsSection.style.display = 'flex';
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
}
|
|
}
|
|
|
|
function updateAppStatus(data) {
|
|
for (const [appKey, status] of Object.entries(data)) {
|
|
// Update status text
|
|
const statusDiv = document.querySelector(`#apps .app:has(#start-${appKey}) .status`);
|
|
if (statusDiv) {
|
|
statusDiv.textContent = `Status: ${status}`;
|
|
}
|
|
|
|
// Update button states
|
|
const startButton = document.getElementById(`start-${appKey}`);
|
|
const stopButton = document.getElementById(`stop-${appKey}`);
|
|
const openButton = document.getElementById(`open-${appKey}`);
|
|
|
|
if (startButton) {
|
|
startButton.disabled = (status === 'running');
|
|
}
|
|
if (stopButton) {
|
|
stopButton.disabled = (status === 'stopped');
|
|
}
|
|
if (openButton) {
|
|
openButton.disabled = (status === 'stopped');
|
|
}
|
|
}
|
|
}
|
|
|
|
function viewLogs(appKey, appName) {
|
|
currentLogApp = appKey;
|
|
currentLogAppName = appName;
|
|
document.getElementById('currentAppName').textContent = `Logs - ${appName}`;
|
|
document.getElementById('downloadLogsBtn').style.display = 'flex';
|
|
updateLogs();
|
|
|
|
// Make sure logs section is visible
|
|
const logsSection = document.querySelector('.logs-section');
|
|
if (logsSection) {
|
|
logsSection.style.display = 'flex';
|
|
}
|
|
}
|
|
|
|
async function stopApp(appKey) {
|
|
try {
|
|
if (appKey == 'bkohya') {
|
|
//port = app_config['port']
|
|
port = 6006;
|
|
|
|
const responseTB = await fetch(`/force_kill_by_port/${port}`, { method: 'GET' });
|
|
const dataTB = await responseTB.json();
|
|
if (dataTB.status === 'killed') {
|
|
alert('TENSORBOARD app found and killed!');
|
|
}
|
|
}
|
|
|
|
const response = await fetch(`/stop/${appKey}`, { method: 'GET' });
|
|
const data = await response.json();
|
|
if (data.status === 'stopped' || data.status === 'already_stopped') {
|
|
// Update button states
|
|
const startButton = document.getElementById(`start-${appKey}`);
|
|
const stopButton = document.getElementById(`stop-${appKey}`);
|
|
const openButton = document.getElementById(`open-${appKey}`);
|
|
|
|
if (startButton) startButton.disabled = false; // Enable start button
|
|
if (stopButton) stopButton.disabled = true; // Disable stop button
|
|
if (openButton) openButton.disabled = true; // Disable open button
|
|
|
|
// Update status text
|
|
const statusDiv = document.querySelector(`#apps .app:has(#start-${appKey}) .status`);
|
|
if (statusDiv) {
|
|
statusDiv.textContent = 'Status: stopped';
|
|
}
|
|
} else if (data.status === 'error') {
|
|
alert(data.message);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
}
|
|
}
|
|
|
|
async function openApp(appKey, port) {
|
|
// *** lutzapps - support to run locally and new support for bkohya gradio url
|
|
// 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
|
|
}
|
|
|
|
// new: support gradio url for kohya_ss, e.g. https://b6365c256c395e755b.gradio.live
|
|
//if (`${appKey}` === 'bkohya' && `${app_status[appKey]['status']}` === 'running') { // get the latest data from the server
|
|
if (appKey == 'bkohya') { // get the latest data from the server
|
|
var response = await fetch('/get_bkohya_launch_url', { method: 'GET' });
|
|
var result = await response.json();
|
|
|
|
var launch_mode = result['mode']; // 'gradio' or 'local'
|
|
//alert('launch_mode=' + launch_mode);
|
|
|
|
var launch_url = result['url'];
|
|
//alert('launch_url=' + launch_url);
|
|
|
|
if (launch_url !== '') { // when a launch url is defined, the app is initialized and ready to be opened
|
|
if (launch_mode === 'gradio') { // if it is a gradio url
|
|
url = launch_url; // then use it, instead of the above defined CF proxy url
|
|
//alert('using gradio for bkohya: ' + launch_url);
|
|
}
|
|
// else use the CF proxy url defined above for localhost, instead of the localhost url from the log
|
|
}
|
|
else { // empty launch_url, waiting for launch url from log
|
|
if (launch_mode === 'gradio') {
|
|
alert('Waiting for Gradio URL to be generated ...');
|
|
}
|
|
else {
|
|
alert('Waiting for local Launch URL to be generated ...');
|
|
}
|
|
return; // no lauch Url yet
|
|
}
|
|
|
|
// open additional tensorboard url
|
|
tensorboard_url = "http://localhost:6006/tensorboard";
|
|
window.open(tensorboard_url, target='_blank', rel='noopener noreferrer');
|
|
}
|
|
|
|
//alert(`openApp URL=${url}`);
|
|
|
|
window.open(url, target='_blank', rel='noopener noreferrer'); // open app window as popup window (need to allow popup-windows in Chrome!)
|
|
}
|
|
|
|
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 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';
|
|
}
|
|
}
|
|
|
|
async function forceKillApp(appKey) {
|
|
if (confirm(`Are you sure you want to FORCE KILL the app '${appKey}'?\nThis may cause data loss!`)) {
|
|
try {
|
|
const response = await fetch(`/force_kill/${appKey}`, { method: 'GET' });
|
|
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}`);
|
|
}
|
|
await updateStatus();
|
|
}
|
|
}
|
|
|
|
async function checkAppInstallation(appKey) {
|
|
//alert('check ' + appKey);
|
|
|
|
try {
|
|
// Update status text
|
|
const statusDiv = document.querySelector(`#apps .app:has(#check-installation-${appKey}) .status`);
|
|
if (statusDiv) {
|
|
statusDiv.textContent = 'Status: checking ...';
|
|
}
|
|
|
|
const response = await fetch(`/check_installation/${appKey}`, { method: 'GET' });
|
|
const data = await response.json();
|
|
if (data.status === 'checked') {
|
|
// Update status text
|
|
if (statusDiv) {
|
|
statusDiv.textContent = 'Status: checked successfully';
|
|
}
|
|
alert(data.message) // alert the details of the successful verification
|
|
} else if (data.status === 'error') {
|
|
// Update status text
|
|
if (statusDiv) {
|
|
statusDiv.textContent = 'Status: check was unsuccessful!';
|
|
}
|
|
|
|
//alert(data.message);
|
|
// send the failed check to confirm for delete the app
|
|
deleteAppInstallation(appKey, data.message);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
alert(`Error checking ${appKey}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
async function refreshAppInstallation(appKey) {
|
|
//alert('refresh ' + appKey);
|
|
message = `REFRESH '${appKey}'\n\n` +
|
|
`Refreshing '${appKey}' needs to 'reset' its status to the state, as when it was last installed/cloned!\n\n` +
|
|
`That means that any changes in the 'app_path' (existing files edited or new files added) get lost, ` +
|
|
`including local model downloads into the various 'models' sub-folders of the app!\n\n` +
|
|
`Before the refreshing starts, the 'Refresh Symlinks' code will be called, to 'pull-back' any locally downloaded model files, ` +
|
|
`and save them into the 'shared_models' workspace folder, before the actual 'reset' is done.\n\n` +
|
|
|
|
`So this operation is not 'light' and you should plan for that accordingly!`;
|
|
message += '\n\n'; // add 2 lines after such message and before the refresh confirm message
|
|
|
|
if (confirm(`${message}Are you sure you want to REFRESH the app '${appKey}'?\nThis may cause data loss!`)) {
|
|
try {
|
|
// Update status text
|
|
const statusDiv = document.querySelector(`#apps .app:has(#refresh-installation-${appKey}) .status`);
|
|
if (statusDiv) {
|
|
statusDiv.textContent = 'Status: refreshing ...';
|
|
}
|
|
|
|
const refreshAppButton = document.getElementById('refresh-installation-' + 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';
|
|
refreshAppButton.parentNode.insertBefore(logsContainer, refreshAppButton.nextSibling);
|
|
}
|
|
|
|
refreshAppButton.disabled = true;
|
|
logsContainer.style.display = 'block';
|
|
appendToInstallLogs({app_name: appKey, log: "Starting to refresh app installation ..."});
|
|
|
|
const response = await fetch(`/refresh_installation/${appKey}`, { method: 'GET' });
|
|
const data = await response.json();
|
|
|
|
if (data.status === 'refreshed') {
|
|
if (statusDiv) {
|
|
statusDiv.textContent = 'Status: refreshed successfully';
|
|
}
|
|
//appendToInstallLogs({app_name: appKey, log: 'Success: ' + data.message});
|
|
alert(data.message);
|
|
} else if (data.status === 'error') {
|
|
if (statusDiv) {
|
|
statusDiv.textContent = 'Status: refresh was unsuccessful!';
|
|
}
|
|
appendToInstallLogs({app_name: appKey, log: 'Error: ' + data.message});
|
|
alert(`Error refreshing '${appKey}': ${data.message}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error refreshing app:', error);
|
|
appendToInstallLogs({app_name: appKey, log: 'Error: ' + error.message});
|
|
alert(`Error refreshing '${appKey}': ${error.message}`);
|
|
} finally {
|
|
refreshAppButton.disabled = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
async function deleteAppInstallation(appKey, checkAppMessage) {
|
|
if (checkAppMessage != '') // if there is a informational message from a failed checkAppInstallation()
|
|
checkAppMessage += '\n\n';
|
|
|
|
message = `DELETE '${appKey}'\n\n` +
|
|
`Deleting '${appKey}' will remove the 'app_path', including local model downloads into the 'models sub-folders of the app!\n` +
|
|
`Before deleting, the 'Refresh Symlinks' code will be called to 'pull-back' any locally downloaded model files, ` +
|
|
`and save them into the 'shared_models' workspace folder, before the actual deletion is done.\n\n` +
|
|
|
|
`So even when minimizing data loss, you should plan for that accordingly!`;
|
|
message = checkAppMessage + message + '\n\n'; // add 2 lines after such message and before the delete confirm message
|
|
|
|
if (confirm(`${message}Are you sure you want to DELETE the app '${appKey}'?\nThis may cause data loss!`)) {
|
|
try {
|
|
// Update status text
|
|
const statusDiv = document.querySelector(`#apps .app:has(#delete-installation-${appKey}) .status`);
|
|
if (statusDiv) {
|
|
statusDiv.textContent = 'Status: deleting ...';
|
|
}
|
|
|
|
const response = await fetch(`/delete_app/${appKey}`, { method: 'GET' });
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
const data = await response.json();
|
|
if (data.status === 'deleted') {
|
|
if (statusDiv) {
|
|
statusDiv.textContent = 'Status: deleted successfully';
|
|
}
|
|
// successfully deleted the app, show summary message
|
|
alert(data.message);
|
|
// refresh the page as app UI is now obsolate
|
|
// remove hashtag url part, keep previous load in browser history (assign instead of replace)
|
|
window.top.location.assign(window.top.location.href.split('#')[0]);
|
|
} else if (data.status === 'error') {
|
|
if (statusDiv) {
|
|
statusDiv.textContent = 'Status: delete was unsuccessful!';
|
|
}
|
|
alert(`Error deleting '${appKey}': ${data.message}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
alert(`Error deleting '${appKey}': ${error.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function showVenvChoiceDialog(appKey, available_venvs) {
|
|
const dialogContent = `
|
|
<h3>For '${appKey}' there are multiple virtual environments available. Please choose one:</h3>
|
|
<ul>
|
|
${available_venvs.map((venv, index) => `
|
|
<li>
|
|
<button onclick="selectVenv('${appKey}', '${venv.version}')">
|
|
'${venv.version}' version (Size: ${formatSize(venv.venv_uncompressed_size_kb * 1024)}) ${venv.download_url}<br>
|
|
${venv.notes}<br>
|
|
${venv.build_info}
|
|
</button>
|
|
</li>
|
|
`).join('')}
|
|
</ul>
|
|
`;
|
|
|
|
showDialog(dialogContent);
|
|
}
|
|
|
|
function selectVenv(appKey, venvVersion) {
|
|
//alert('selected ' + venvVersion);
|
|
document.body.removeChild(document.body.lastChild); // Remove the dialog
|
|
//const modal = document.querySelector('.modal');
|
|
//document.body.removeChild(modal);
|
|
|
|
installApp(appKey, venvVersion);
|
|
}
|
|
|
|
function initializeUI() {
|
|
updateStatus();
|
|
// TODO: need a way this 2 functions not pollute the logs every second or 5 seconds
|
|
setInterval(updateStatus, 5000);
|
|
setInterval(updateLogs, 1000);
|
|
|
|
// Initialize model types
|
|
refreshModelTypes();
|
|
}
|
|
|
|
async function refreshModelTypes() {
|
|
var data = {};
|
|
data.cmd = 'refreshModelTypes';
|
|
await 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');
|
|
if (!modelTypeSelect) {
|
|
console.error('Model type select element not found');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// get the data from the Server
|
|
response = await fetch('/get_model_types');
|
|
result = await response.json();
|
|
|
|
var model_types = result; // get the JSON-Object
|
|
var count = Object.keys(model_types).length;
|
|
|
|
var modelTypeSelected = modelTypeSelect.value; // remember the current selected modelType.option value
|
|
modelTypeSelect.options.length = 0; // clear all current modelTypeSelect options
|
|
|
|
if (count === 0) {
|
|
// Add a default option if no model types are available
|
|
const defaultOption = document.createElement('option');
|
|
defaultOption.text = 'Please create shared folders first';
|
|
defaultOption.value = '';
|
|
modelTypeSelect.add(defaultOption);
|
|
return;
|
|
}
|
|
|
|
for (i = 0; i < count; i++) {
|
|
modelTypeOption = document.createElement('option');
|
|
modelType = model_types[String(i)]['modelfolder'];
|
|
modelTypeOption.setAttribute('value', modelType);
|
|
modelTypeOption.appendChild(document.createTextNode(model_types[String(i)]['desc']));
|
|
modelTypeSelect.appendChild(modelTypeOption);
|
|
}
|
|
|
|
if (modelTypeSelected === "") {
|
|
modelTypeSelect.selectedIndex = 0;
|
|
MODELTYPE_SELECTED = modelTypeSelect.options[0].value;
|
|
} else {
|
|
modelTypeSelect.value = modelTypeSelected;
|
|
MODELTYPE_SELECTED = modelTypeSelected;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error refreshing model types:', error);
|
|
}
|
|
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
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
await startModelDownload(url, modelName, modelType, civitaiToken, hfToken);
|
|
}
|
|
|
|
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
|
|
}),
|
|
});
|
|
|
|
const result = await response.json();
|
|
if (result.status === 'success') {
|
|
statusDiv.textContent = result.message;
|
|
progressBar.style.width = '100%';
|
|
progressBar.textContent = '100%';
|
|
await loadModelFolders();
|
|
// showRecreateSymlinksButton(); // disable call since it's automatic now
|
|
} 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 showDialog(modalContent) {
|
|
const modal = document.createElement('div');
|
|
modal.className = 'modal';
|
|
modal.innerHTML = `
|
|
<div class="modal-content">
|
|
<span class="close">×</span>
|
|
${modalContent}
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(modal);
|
|
|
|
modal.style.display = 'block';
|
|
|
|
const closeBtn = modal.querySelector('.close');
|
|
closeBtn.onclick = function() {
|
|
document.body.removeChild(modal);
|
|
// refresh the page if user cancel and close the dialog without selecting a venv
|
|
// remove hashtag url part, keep previous load in browser history (assign instead of replace)
|
|
window.top.location.assign(window.top.location.href.split('#')[0]);
|
|
}
|
|
|
|
window.onclick = function(event) {
|
|
if (event.target == modal) {
|
|
document.body.removeChild(modal);
|
|
}
|
|
}
|
|
}
|
|
|
|
function showVersionChoiceDialog(data, url, modelName, modelType, civitaiToken, hfToken) {
|
|
const dialogContent = `
|
|
<h3>Multiple versions for this model available. Please choose one:</h3>
|
|
<ul>
|
|
${data.versions.map((version, index) => `
|
|
<li>
|
|
<button onclick="selectVersion(${version.id}, '${url}', '${modelName}', '${modelType}', '${civitaiToken}', '${hfToken}')">
|
|
${version.baseModel} '${version.name}' (Created: ${new Date(version.createdAt).toLocaleString()})
|
|
</button>
|
|
</li>
|
|
`).join('')}
|
|
</ul>
|
|
`;
|
|
|
|
showDialog(dialogContent);
|
|
}
|
|
|
|
function showFileChoiceDialog(data, url, modelName, modelType, civitaiToken, hfToken) {
|
|
const dialogContent = `
|
|
<h3>Multiple files for this model 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.sizeKB * 1024)}, Type: ${file.type}, Format: ${file.format}, Size: ${file.size}, FP: ${file.fp})
|
|
</button>
|
|
</li>
|
|
`).join('')}
|
|
</ul>
|
|
`;
|
|
|
|
showDialog(dialogContent);
|
|
}
|
|
|
|
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) {
|
|
document.body.removeChild(document.body.lastChild); // Remove the dialog
|
|
//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: 'GET' });
|
|
const data = await response.json();
|
|
if (data.status === 'success') {
|
|
statusElement.textContent = data.message;
|
|
} else {
|
|
statusElement.textContent = 'Error: ' + data.message;
|
|
}
|
|
} catch (error) {
|
|
statusElement.textContent = 'Error: ' + error.message;
|
|
}
|
|
}
|
|
|
|
async function saveCivitaiToken() {
|
|
// Get token from the main Civitai token input field
|
|
const token = document.getElementById('civitaiToken').value;
|
|
if (!token) {
|
|
alert('Please enter a Civitai API token');
|
|
return;
|
|
}
|
|
|
|
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') {
|
|
// Update both token fields to show it's saved
|
|
document.getElementById('civitaiToken').value = '********';
|
|
document.getElementById('civitaiTokenSave').value = '********';
|
|
alert('Civitai token saved successfully.');
|
|
} else {
|
|
alert(`Error: ${result.message}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
alert(`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 = '********';
|
|
}
|
|
} 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);
|
|
}
|
|
}
|
|
|
|
function formatSize(bytes) {
|
|
if (!bytes || isNaN(bytes)) return '0 B';
|
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), sizes.length - 1);
|
|
const size = bytes / Math.pow(1024, i);
|
|
return `${size.toFixed(2)} ${sizes[i]}`;
|
|
}
|
|
|
|
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
|
|
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);
|
|
});
|
|
}
|
|
|
|
// Call this function when the Models tab is opened
|
|
document.querySelector('.navbar-tabs a[onclick="openTab(event, \'models-tab\')"]').addEventListener('click', function() {
|
|
loadModelFolders();
|
|
refreshModelTypes(); // Refresh model types when switching to Models tab
|
|
loadCivitaiToken();
|
|
loadHFToken();
|
|
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
|
|
} 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
|
|
} else if (tabName === 'settings-tab') {
|
|
loadSshDetails();
|
|
// Remove updateFileBrowserStatus() from here
|
|
}
|
|
}
|
|
|
|
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.');
|
|
});
|
|
}
|
|
|
|
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)";
|
|
}
|
|
}
|
|
}
|
|
|
|
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.\nAre you sure you want to SET a password?')) {
|
|
try {
|
|
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)';
|
|
} else {
|
|
alert('Failed to set SSH password: ' + result.message);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error setting SSH password:', error);
|
|
alert('An error occurred while setting the SSH password.');
|
|
}
|
|
}
|
|
}
|
|
|
|
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}`);
|
|
}
|
|
|
|
window.open(url, '_blank');
|
|
}
|
|
|
|
async function controlFileBrowser(action) {
|
|
try {
|
|
const response = await fetch(`/${action}_filebrowser`);
|
|
const result = await response.json();
|
|
if (result.status === 'started' || result.status === 'stopped') {
|
|
// Remove updateFileBrowserStatus() from here
|
|
}
|
|
} catch (error) {
|
|
console.error('Error controlling File Browser:', error);
|
|
}
|
|
}
|
|
|
|
// Reduce the frequency of status updates
|
|
// Remove this interval
|
|
// setInterval(updateFileBrowserStatus, 30000); // Check every 30 seconds instead of 5 seconds
|
|
|
|
// Update the DOMContentLoaded event listener
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadSshDetails();
|
|
// ... (other initialization code)
|
|
});
|
|
|
|
// Update the Settings tab click handler
|
|
document.querySelector('.navbar-tabs a[onclick="openTab(event, \'settings-tab\')"]').addEventListener('click', function() {
|
|
loadSshDetails();
|
|
// Remove updateFileBrowserStatus() from here
|
|
});
|
|
|
|
async function installApp(appKey, venvVersion) {
|
|
//alert('install ' + appKey + ' (' + venvVersion + ')');
|
|
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 {
|
|
if (venvVersion == 'undefined' || venvVersion == '') {
|
|
const response = await fetch(`/available_venvs/${appKey}`, { method: 'GET' });
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.status == 'error') {
|
|
throw new Error(data.error['message']);
|
|
}
|
|
|
|
availableVenvs = data.available_venvs;
|
|
if (availableVenvs.length == 1) // user already selected a version with app-specific ENV var or DEBUG_SETTINGS
|
|
installApp(appKey, availableVenvs[0].version); // call ourself with the ONE pre-selection, no need to ask user again
|
|
else
|
|
// show the Venv-Picker dialog so the user can choose which VENV to install for this app
|
|
venvVersion = showVenvChoiceDialog(appKey, availableVenvs);
|
|
// this will call-back here with a selected venvVersion
|
|
|
|
return; // not continue the flow from here
|
|
}
|
|
|
|
// here we have a user-selected venvVersion to install
|
|
const response = await fetch(`/install/${appKey}/${venvVersion}`, { method: 'GET' });
|
|
|
|
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}`});
|
|
}
|
|
// 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') {
|
|
// Reset heartbeat timeout on receiving heartbeat response
|
|
resetHeartbeatTimeout();
|
|
} 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);
|
|
}
|
|
// Handle other message types as needed
|
|
} catch (error) {
|
|
console.error('Error parsing WebSocket message:', error, 'Raw message:', event.data);
|
|
}
|
|
};
|
|
|
|
function updateInstallProgress(data) {
|
|
const progressContainer = document.getElementById(`install-progress-${data.app_name}`);
|
|
const downloadProgress = progressContainer.querySelector('.download-progress');
|
|
const unpackProgress = progressContainer.querySelector('.unpack-progress');
|
|
const cloneProgress = progressContainer.querySelector('.clone-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(0)}%`;
|
|
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(0)}%`;
|
|
speedDisplay.textContent = `Processing: ${data.processed}`;
|
|
etaDisplay.textContent = '';
|
|
} else if (data.stage === 'Cloning') {
|
|
cloneProgress.style.width = `${data.percentage}%`;
|
|
cloneProgress.textContent = `${data.percentage.toFixed(0)}%`;
|
|
speedDisplay.textContent = `Processing: ${data.processed}`;
|
|
etaDisplay.textContent = '';
|
|
} else if (data.stage === 'Download Complete') {
|
|
downloadProgress.style.width = '100%';
|
|
downloadProgress.textContent = '100%';
|
|
speedDisplay.textContent = '';
|
|
etaDisplay.textContent = '';
|
|
} else if (data.stage === 'Unpacking Complete') {
|
|
unpackProgress.style.width = '100%';
|
|
unpackProgress.textContent = '100%';
|
|
speedDisplay.textContent = '';
|
|
etaDisplay.textContent = '';
|
|
} else if (data.stage === 'Cloning Complete') {
|
|
cloneProgress.style.width = '100%';
|
|
cloneProgress.textContent = '100%';
|
|
speedDisplay.textContent = '';
|
|
etaDisplay.textContent = '';
|
|
}
|
|
|
|
stageDisplay.textContent = `Stage: ${data.stage}`;
|
|
}
|
|
|
|
function formatTime(seconds) {
|
|
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}`);
|
|
installButton.disabled = false; // Re-enable the button only on failure
|
|
}
|
|
}
|
|
|
|
async function fixCustomNodes(appKey) {
|
|
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: 'GET' });
|
|
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;
|
|
}
|
|
}
|
|
|
|
// Poddy animation
|
|
const secretSequence = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65];
|
|
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);
|
|
}
|
|
|
|
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);
|
|
|
|
async function createSharedFolders() {
|
|
const statusElement = document.getElementById('shared-folders-status');
|
|
statusElement.textContent = 'Creating shared folders ...';
|
|
try {
|
|
const response = await fetch('/create_shared_folders', { method: 'GET' });
|
|
const data = await response.json();
|
|
if (data.status === 'success') {
|
|
statusElement.textContent = data.message;
|
|
} else {
|
|
statusElement.textContent = 'Error: ' + data.message;
|
|
}
|
|
} catch (error) {
|
|
statusElement.textContent = 'Error: ' + error.message;
|
|
}
|
|
}
|
|
|
|
function switchInnerTab(evt, tabName) {
|
|
const innerTabContents = document.getElementsByClassName('inner-tab-content');
|
|
for (let content of innerTabContents) {
|
|
content.classList.remove('active');
|
|
}
|
|
|
|
const innerTabLinks = document.getElementsByClassName('inner-tab-link');
|
|
for (let link of innerTabLinks) {
|
|
link.classList.remove('active');
|
|
}
|
|
|
|
document.getElementById(tabName).classList.add('active');
|
|
evt.currentTarget.classList.add('active');
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |