Commit 1d416a3b by Jonathan Thomas

Adding auto-export on first export modal view, and a new demo-launch component…

Adding auto-export on first export modal view, and a new demo-launch component to give instructions on launching your own instance.
parent b0db20ad
...@@ -96,7 +96,12 @@ ...@@ -96,7 +96,12 @@
<div class="modal-body"> <div class="modal-body">
<!-- Progress Bar for Export --> <!-- Progress Bar for Export -->
<div v-show="!current_export.output" class="col-sm-12"> <div v-show="isExportInProgress" class="col-sm-12 mb-2">
<p class="text-muted small mb-1">Please wait while your video is being exported.</p>
</div>
<!-- Progress Bar for Export -->
<div v-show="isExportInProgress" class="col-sm-12">
<div class="progress align-middle"> <div class="progress align-middle">
<div class="progress-bar" role="progressbar" v-bind:style="{ width: current_export.progress + '%'}" :aria-valuenow="current_export.progress" aria-valuemin="0" aria-valuemax="100"></div> <div class="progress-bar" role="progressbar" v-bind:style="{ width: current_export.progress + '%'}" :aria-valuenow="current_export.progress" aria-valuemin="0" aria-valuemax="100"></div>
</div> </div>
...@@ -109,12 +114,66 @@ ...@@ -109,12 +114,66 @@
</video> </video>
</div> </div>
<div v-if="isDemoMode" class="col-sm-12 mt-4">
<CloudInstancePromo
compact
label="Demo Preview"
title="Want to export longer videos?"
:subtitle-html="exportSubtitleHtml"
/>
</div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer d-flex align-items-center gap-2 flex-wrap">
<button type="button" class="btn btn-secondary" ref="Close" data-bs-dismiss="modal">Cancel</button> <button type="button"
<a v-if="current_export.output" :href="current_export.output" class="btn btn-info" download target="_blank">Download</a> class="btn btn-outline-secondary me-auto"
<button v-if="!current_export.output" type="button" class="btn btn-primary" @click="startExport" :disabled="exporting">Export</button> ref="Close"
<button v-if="current_export.output" type="button" class="btn btn-primary" @click="startExport" :disabled="exporting">Export Again</button> data-bs-dismiss="modal">
Cancel
</button>
<div class="d-flex align-items-center gap-2">
<div v-if="current_export.output" class="btn-group">
<button type="button"
class="btn btn-success dropdown-toggle"
data-bs-toggle="dropdown"
aria-expanded="false">
Download
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a class="dropdown-item"
:href="current_export.output"
download>
Download Video (*.mp4)
</a>
</li>
<li>
<a class="dropdown-item"
:href="projectDownloadUrl || '#'"
target="_blank"
rel="noopener">
Download Project (*.osp)
</a>
</li>
</ul>
</div>
<button
v-if="!current_export.output"
type="button"
class="btn btn-primary"
@click="startExport"
:disabled="exporting">
Export
</button>
<button
v-if="current_export.output"
type="button"
class="btn btn-primary"
@click="startExport"
:disabled="exporting">
Export Again
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -131,13 +190,15 @@ ...@@ -131,13 +190,15 @@
import {mapActions, mapState, mapMutations, mapGetters} from "vuex" import {mapActions, mapState, mapMutations, mapGetters} from "vuex"
import { Modal } from "bootstrap" import { Modal } from "bootstrap"
import ClipEffectsModal from "./ClipEffectsModal" import ClipEffectsModal from "./ClipEffectsModal"
import CloudInstancePromo from "./CloudInstancePromo.vue"
import { createClipWithEffectsForm, hasClipEffectsChanges } from "../utils/clipEffects" import { createClipWithEffectsForm, hasClipEffectsChanges } from "../utils/clipEffects"
export default { export default {
name: "Clips.vue", name: "Clips.vue",
props: ['project'], props: ['project'],
components: { components: {
ClipEffectsModal ClipEffectsModal,
CloudInstancePromo
}, },
data() { data() {
return { return {
...@@ -149,7 +210,8 @@ export default { ...@@ -149,7 +210,8 @@ export default {
dragOverPosition: null, dragOverPosition: null,
autoScrollDirection: 0, autoScrollDirection: 0,
autoScrollFrame: null, autoScrollFrame: null,
dragListenersActive: false dragListenersActive: false,
autoExportedProjectId: null
} }
}, },
methods: { methods: {
...@@ -427,6 +489,9 @@ export default { ...@@ -427,6 +489,9 @@ export default {
}, },
async startExport() { async startExport() {
this.exporting = true this.exporting = true
if (this.$store && this.$store.commit) {
this.$store.commit('setExport', { progress: 0.0 })
}
let payload = { let payload = {
"export_type": "video", "export_type": "video",
"video_format": "mp4", "video_format": "mp4",
...@@ -474,6 +539,26 @@ export default { ...@@ -474,6 +539,26 @@ export default {
showExportModal() { showExportModal() {
let myModal = new Modal(this.$refs.exportModal) let myModal = new Modal(this.$refs.exportModal)
myModal.show() myModal.show()
this.$nextTick(() => {
this.maybeAutoExport()
})
},
maybeAutoExport() {
if (!this.project || !this.project.id) {
return
}
if (this.autoExportedProjectId === this.project.id) {
return
}
const hasExistingExport = !!(this.current_export?.id || this.current_export?.output || (this.current_export && this.current_export.progress > 0))
if (hasExistingExport) {
this.autoExportedProjectId = this.project.id
return
}
this.autoExportedProjectId = this.project.id
if (!this.exporting) {
this.startExport()
}
}, },
...mapActions(['loadClips', 'createClip', 'deleteClip', 'editClip', 'moveClip', 'createExport']), ...mapActions(['loadClips', 'createClip', 'deleteClip', 'editClip', 'moveClip', 'createExport']),
...mapMutations((['setPreviewClip', 'setClips', 'setScrollToClip'])) ...mapMutations((['setPreviewClip', 'setClips', 'setScrollToClip']))
...@@ -483,6 +568,40 @@ export default { ...@@ -483,6 +568,40 @@ export default {
...mapState(['clips', 'preview', 'scrollToClip', 'current_export']), ...mapState(['clips', 'preview', 'scrollToClip', 'current_export']),
isDragging() { isDragging() {
return !!this.draggingClip return !!this.draggingClip
},
isDemoMode() {
const apiUrl = (process.env.VUE_APP_OPENSHOT_API_URL || '').trim()
if (!apiUrl) {
return false
}
const normalized = apiUrl.replace(/\/+$/, '').toLowerCase()
const host = normalized.replace(/^https?:\/\//, '')
return host.startsWith('cloud.openshot.org')
},
isExportInProgress() {
const hasPendingRecord = !!(this.current_export && this.current_export.id && !this.current_export.output)
return !!this.exporting || hasPendingRecord
},
exportSubtitleHtml() {
const link = '<a href="https://www.openshot.org/cloud-api/" target="_blank" rel="noopener">OpenShot Cloud API</a>'
return `Deploy ${link} on AWS, Azure, or Google Cloud to access full-length exports, scalable rendering resources, persistent project storage, and a reliable cloud backend for your video production pipeline.`
},
projectDownloadUrl() {
if (!this.project) {
return null
}
if (this.project.url) {
try {
const base = this.project.url.endsWith('/') ? this.project.url : `${this.project.url}/`
return new URL('download/', base).toString()
} catch (err) {
// fall through to ID-based path below
}
}
if (this.project.id) {
return `/projects/${this.project.id}/download/`
}
return null
} }
}, },
watch: { watch: {
......
<template>
<div :class="['cloud-instance-promo card border-0 shadow-sm', { 'promo-compact': compact }]">
<div :class="['card-body', compact ? 'py-3 px-3' : 'py-4 px-4']">
<div class="row g-3 align-items-center">
<div class="col-12 col-md-7 text-center text-md-start">
<p class="text-uppercase text-muted fw-semibold small mb-1">{{ label }}</p>
<h6 class="fw-semibold mb-1">{{ title }}</h6>
<p v-if="subtitleHtml" class="text-muted small mb-0" v-html="subtitleHtml"></p>
<p v-else-if="subtitle" class="text-muted small mb-0">{{ subtitle }}</p>
</div>
<div class="col-12 col-md-5">
<div class="d-flex flex-column align-items-center gap-2">
<a
v-for="provider in providers"
:key="provider.name"
class="btn btn-light border promo-provider-btn w-75 mx-auto"
:href="provider.href"
:target="provider.target"
:rel="provider.rel"
:title="provider.title"
>
<img :src="provider.icon" :alt="provider.name" height="22">
</a>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "CloudInstancePromo",
props: {
label: {
type: String,
default: "Ready for more?"
},
title: {
type: String,
default: "Launch your own instance"
},
subtitle: {
type: String,
default: "Deploy OpenShot Cloud API on AWS, Azure, or Google Cloud to access full-length exports, scalable rendering resources, persistent project storage, and a reliable cloud backend for your video production pipeline."
},
subtitleHtml: {
type: String,
default: ""
},
compact: {
type: Boolean,
default: false
}
},
computed: {
providers() {
return [
{
name: "AWS",
icon: "//cdn.openshot.org/static/img/icons/aws.svg",
href: "https://aws.amazon.com/marketplace/pp/B074H87FSJ/",
target: "_blank",
rel: "noopener",
title: "Launch Instance: AWS"
},
{
name: "Azure",
icon: "//cdn.openshot.org/static/img/icons/azure.svg",
href: "https://azuremarketplace.microsoft.com/en-us/marketplace/apps/openshotstudiosllc.openshot-cloud-api",
target: "_blank",
rel: "noopener",
title: "Launch Instance: Azure"
},
{
name: "Google Cloud",
icon: "//cdn.openshot.org/static/img/icons/google.svg",
href: "https://console.cloud.google.com/marketplace/product/openshotstudios-public/openshot-video-editing-cloud-api",
target: "_blank",
rel: "noopener",
title: "Launch Instance: Google Cloud"
}
]
}
}
}
</script>
<style scoped>
.cloud-instance-promo {
background: linear-gradient(180deg, #ffffff 0%, #f8f9fa 100%);
border-radius: 1rem;
}
.promo-compact {
padding: 0;
}
.promo-provider-btn {
padding: 0.45rem 0.75rem;
border-radius: 999px;
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.promo-provider-btn:hover {
transform: translateY(-1px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
}
</style>
<template> <template>
<!-- Login container --> <!-- Login container -->
<div v-if="getCloudApiUrl == 'https://cloud.openshot.org'" class="row ps-5 pe-5 justify-content-center"> <div v-if="isDemoEnvironment" class="row ps-5 pe-5 justify-content-center">
<div class="col-xs-12 col-md-6 col-lg-4 text-center"> <div class="col-xs-12 col-md-8 col-lg-6">
<div class="alert alert-primary" role="alert"> <div class="alert alert-primary" role="alert">
<h4>Demo Credentials:</h4> <h4 class="mb-2">Demo Credentials</h4>
<strong>User:</strong> demo-cloud, <strong>Password:</strong> demo-password <p class="mb-2">
<p class="pt-3 fw-light">All demo projects are deleted every 12 hours, and are limited to 4 seconds in length.</p> <strong>User:</strong> demo-cloud,
<div> <strong>Password:</strong> demo-password
<p class="m-0"><strong>Launch</strong> an instance (with no limitations):</p> </p>
<a class="btn btn-lg btn-primary launch-instance" href="https://aws.amazon.com/marketplace/pp/B074H87FSJ/" target="_blank" style="margin: 5px; background-color: #ffffff;" title="Launch Instance: AWS"><img src="//cdn.openshot.org/static/img/icons/aws.svg" height="24"></a> <CloudInstancePromo
<a class="btn btn-lg btn-primary launch-instance" href="https://azuremarketplace.microsoft.com/en-us/marketplace/apps/openshotstudiosllc.openshot-cloud-api" target="_blank" style="margin: 5px; background-color: #ffffff;" title="Launch Instance: Azure"><img src="//cdn.openshot.org/static/img/icons/azure.svg" height="24"></a> label="Ready for more?"
<a class="btn btn-lg btn-primary launch-instance" href="javascript:alert('Google Cloud - Coming Soon...')" style="margin: 5px; background-color: #ffffff;" title="Launch Instance: Google Cloud"><img src="//cdn.openshot.org/static/img/icons/google.svg" height="24"></a> title="Launch your own instance"
</div> :subtitle-html="launchSubtitleHtml"
/>
</div> </div>
</div> </div>
</div> </div>
...@@ -34,9 +35,13 @@ ...@@ -34,9 +35,13 @@
<script> <script>
import { mapActions, mapGetters, mapMutations } from 'vuex' import { mapActions, mapGetters, mapMutations } from 'vuex'
import CloudInstancePromo from '../components/CloudInstancePromo.vue'
export default { export default {
name: "Login.vue", name: "Login.vue",
components: {
CloudInstancePromo
},
data() { data() {
return { return {
username: null, username: null,
...@@ -66,6 +71,18 @@ export default { ...@@ -66,6 +71,18 @@ export default {
getCloudApiUrl() { getCloudApiUrl() {
return process.env.VUE_APP_OPENSHOT_API_URL return process.env.VUE_APP_OPENSHOT_API_URL
}, },
isDemoEnvironment() {
const apiUrl = (this.getCloudApiUrl || '').trim()
if (!apiUrl) {
return false
}
const normalized = apiUrl.replace(/\/+$/, '').toLowerCase()
return normalized === 'https://cloud.openshot.org'
},
launchSubtitleHtml() {
const link = '<a href="https://www.openshot.org/cloud-api/" target="_blank" rel="noopener">OpenShot Cloud API</a>'
return `Deploy ${link} on AWS, Azure, or Google Cloud for full-length exports and persistent projects.`
},
...mapGetters(['isAuthenticated']) ...mapGetters(['isAuthenticated'])
}, },
} }
...@@ -73,4 +90,4 @@ export default { ...@@ -73,4 +90,4 @@ export default {
<style scoped> <style scoped>
</style> </style>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment