Commit efde96c4 by Jonathan Thomas

Initial commit of simple-editor demo (in-progress)

parents
VUE_APP_ENV_VARIABLE=hello
VUE_APP_API_URL=https://cloud.openshot.org/
\ No newline at end of file
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
simple-editor
=============
Provide a very simple editor, so end-users can upload files,
create clips, sequence clips, and export a video.
urls:
-----
- /login/: Allow user to login to API (keep track in vuex)
- /login/auto/: Allow a passed token to auto-login
- /projects/: List all projects (create, edit, copy, delete)
- /projects/ID/: Simple editor (files, clips, preview, export)
editor:
-------
- files: upload & delete
- clips: preview file/clip, start/end, position (create, update, delete)
- export: progress bar, cancel, download, preview (show last export)
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "simple-editor",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@popperjs/core": "^2.11.0",
"bootstrap": "^5.1.3",
"core-js": "^3.6.5",
"vue": "^3.0.0",
"vue-router": "^4.0.0-0",
"vuex": "^4.0.0-0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.0.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^7.0.0"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
<template>
<!-- Navigation -->
<header>
<nav class="navbar navbar-expand-lg navbar-light nav-pills">
<div class="container mb-3">
<router-link to="/" class="navbar-brand" aria-current="page">Simple Editor</router-link>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li v-show="!isAuthenticated" class="nav-item">
<router-link to="/login" class="nav-link" aria-current="page">Login</router-link>
</li>
<li v-show="isAuthenticated" class="nav-item">
<router-link to="/" class="nav-link" aria-current="page">Projects</router-link>
</li>
<li v-show="isAuthenticated" class="nav-item">
<router-link to="/about" class="nav-link" aria-current="page">About</router-link>
</li>
<li v-show="isAuthenticated" class="nav-item px-3">
<router-link to="/logout" class="nav-link" aria-current="page">Logout</router-link>
</li>
</ul>
</div>
</div>
</nav>
</header>
<!--router content-->
<div class="container">
<router-view/>
</div>
</template>
<script>
import { mapGetters, mapActions, mapMutations } from 'vuex'
export default {
methods: {
...mapActions(['login']),
...mapMutations(['setUser'])
},
computed: {
...mapGetters(['isAuthenticated'])
},
async created() {
// auto-login on subsequent visits
if (!this.isAuthenticated && localStorage.auth) {
let auth = localStorage.auth
// set auth token in vuex (so other API calls will authenticate)
this.setUser({auth})
// auto-login user (to populate vuex user object)
await this.login(auth)
if (!this.isAuthenticated) {
// login failed, show login page
this.$router.push('/login')
}
}
}
}
</script>
<style scoped>
.navbar-light .navbar-nav .nav-link.active, .navbar-light .navbar-nav .show>.nav-link {
color: #ffffff;
}
</style>
<template>
<div class="row mb-3 p-2 scrolling-container">
<h3>Clips</h3>
<div class="col-12">
<div class="row gy-2 gx-2 mb-2" v-for="clip in clips" :key="clip.id">
<div class="col-4 text-center img-parent">
<img class="img-fluid img-thumbnail clip-thumbnail" :title="clip.name" src="../assets/logo.png"/>
<p class="carousel-caption clip-thumbnail">Start</p>
</div>
<div class="col-4 text-center img-parent">
<img class="img-fluid img-thumbnail clip-thumbnail" :title="clip.name" src="../assets/logo.png"/>
<p class="carousel-caption clip-thumbnail">End</p>
</div>
<div class="col-4">
<div class="row">
<div class="col-12">
<div class="d-grid gap-1 p-2">
<button type="button" class="btn btn-outline-primary">Move Up</button>
<button type="button" class="btn btn-outline-primary">Move Down</button>
<button type="button" class="btn btn-outline-primary">Delete</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Clips.vue",
data() {
return {
clips: [
{ 'id': 1, 'name': 'filename1.mp4', 'start': 0, 'end': 10 },
{ 'id': 2, 'name': 'filename1.mp4', 'start': 3, 'end': 13 },
{ 'id': 3, 'name': 'filename1.mp4', 'start': 0, 'end': 5 },
{ 'id': 4, 'name': 'filename1.mp4', 'start': 10, 'end': 15 }
]
}
}
}
</script>
<style scoped>
.scrolling-container {
height: 300px;
overflow: scroll;
}
.img-parent {
position: relative;
}
.clip-thumbnail {
cursor: pointer;
}
</style>
\ No newline at end of file
<template>
<div class="row mb-3 gy-2 gx-2 p-2 scrolling-container">
<h3>Files <button type="button" class="btn btn-primary upload-btn">Upload</button></h3>
<div class="col-sm-12">
<div class="form-floating mb-3">
<input type="text" class="form-control" id="floatingInput" placeholder="Search">
<label for="floatingInput">Search</label>
</div>
</div>
<div v-for="file in files" :key="file.id" class="col-4" style="overflow: scroll;">
<img @click="toggleSelection(file)" :class="getSelectedClass(file)" class="file-thumbnail img-fluid img-thumbnail" :title="file.name" src="../assets/logo.png"/>
</div>
</div>
</template>
<script>
export default {
name: "Files.vue",
data() {
return {
files: [
{ 'id': 1, 'name': 'filename1.mp4', 'selected': false },
{ 'id': 2, 'name': 'filename1.mp4', 'selected': false },
{ 'id': 3, 'name': 'filename1.mp4', 'selected': false },
{ 'id': 4, 'name': 'filename1.mp4', 'selected': false },
{ 'id': 5, 'name': 'filename1.mp4', 'selected': false },
{ 'id': 6, 'name': 'filename1.mp4', 'selected': false },
{ 'id': 7, 'name': 'filename1.mp4', 'selected': false },
]
}
},
methods: {
toggleSelection(fileObject) {
fileObject.selected = !fileObject.selected
},
getSelectedClass(fileObject) {
if (fileObject.selected) {
return 'selected'
} else {
return ''
}
}
}
}
</script>
<style scoped>
.scrolling-container {
height: 300px;
overflow: scroll;
}
.upload-btn {
float: right;
}
.selected {
border: #0d6efd 4px solid;
}
.file-thumbnail {
cursor: pointer;
}
</style>
\ No newline at end of file
<template>
<h3 class="p-2">Preview <button type="button" class="btn btn-danger export-btn">Export</button></h3>
<video controls="" preload="" poster="http://sandbox.thewikies.com/vfe-generator/images/big-buck-bunny_poster.jpg" class="video-responsive" id="video1" width="100%" height="100%">
<source type="video/mp4" src="http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4">
<source type="video/webm" src="http://clips.vorwaerts-gmbh.de/big_buck_bunny.webm">
<source type="video/ogg" src="http://clips.vorwaerts-gmbh.de/big_buck_bunny.ogv">
<track src="Content/audio/captions.vtt" kind="subtitles" srclang="en" label="English">
<img alt="Big Buck Bunny" src="http://sandbox.thewikies.com/vfe-generator/images/big-buck-bunny_poster.jpg" width="640" height="360" title="No video playback capabilities, please download the video below">
</video>
</template>
<script>
export default {
name: "Preview.vue"
}
</script>
<style scoped>
video {
background-color: #000000;
width: 100%;
height: 100%;
}
.export-btn {
float: right;
}
</style>
\ No newline at end of file
<template>
<div class="mb-5 text-center">
<div class="col-lg-6 mx-auto">
<p class="lead">Create and edit your own video editing projects!</p>
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
<button type="button" class="btn btn-primary btn-lg px-4 gap-3" @click="showNewProjectModal">Create Project</button>
</div>
</div>
</div>
<!-- Project cards -->
<div class="row gy-3 gx-3">
<div class="col-sm-3" v-for="project in projects" :key="project.id" >
<div class="card">
<div class="card-body">
<h5 class="card-title">{{ project.name }}</h5>
<p class="card-text">{{ project.width }}x{{ project.height }} @ {{ project.fps_num / project.fps_den }} FPS</p>
<button type="button" class="btn btn-primary" @click="editProject(project)">Edit</button>
<button type="button" class="btn btn-danger" @click="showDeletePrompt(project)" ref="Delete">Delete</button>
</div>
</div>
</div>
</div>
<!-- New Project Modal -->
<div class="modal fade" ref="newProjectModal" tabindex="-1" aria-labelledby="newModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="newModalLabel">Create Project</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Project Name:</label>
<input v-model="project_name" type="text" class="form-control">
</div>
<label class="form-label">Size:</label>
<div class="input-group mb-3">
<input v-model="project_width" type="text" class="form-control" placeholder="Width">
<span class="input-group-text">x</span>
<input v-model="project_height" type="text" class="form-control" placeholder="Height">
</div>
<label class="form-label">Framerate / FPS:</label>
<div class="input-group mb-3">
<input v-model="project_fps_num" type="text" class="form-control" placeholder="FPS Numerator">
<span class="input-group-text">/</span>
<input v-model="project_fps_den" type="text" class="form-control" placeholder="FPS Denominator">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" ref="CloseProjectModal" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" @click="createProjectClick">Create Project</button>
</div>
</div>
</div>
</div>
<!-- Delete Modal -->
<div class="modal fade" ref="deleteModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Delete Project <span ref="prompt_project_name"></span>?</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
Are you sure you want to delete project #<span ref="prompt_project_id"></span>?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" ref="Close" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" @click="deleteProjectClick">Delete Project</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex'
import { Modal } from "bootstrap"
export default {
name: "Projects.vue",
data() {
return {
deletedProject: null,
project_name: 'New Project',
project_fps_num: 30,
project_fps_den: 1,
project_width: 1920,
project_height: 1080
}
},
methods: {
editProject(project) {
this.$router.push(`/projects/${project.id}`)
},
showNewProjectModal() {
let myModal = new Modal(this.$refs.newProjectModal)
myModal.show()
},
async createProjectClick() {
let payload = {
"name": this.project_name,
"width": this.project_width,
"height": this.project_height,
"fps_num": this.project_fps_num,
"fps_den": this.project_fps_den,
"sample_rate": 44100,
"channels": 2,
"channel_layout": 3,
"json": {}
}
await this.createProject(payload)
this.$refs.CloseProjectModal.click();
},
showDeletePrompt(project) {
this.deletedProject = project;
this.$refs.prompt_project_id.innerText = this.deletedProject.id
this.$refs.prompt_project_name.innerText = this.deletedProject.name
let myModal = new Modal(this.$refs.deleteModal)
myModal.show()
},
deleteProjectClick() {
this.deleteProject(this.deletedProject.id)
this.$refs.Close.click();
this.deletedProject = null
},
...mapActions(['loadProjects', 'createProject', 'deleteProject'])
},
computed: {
...mapState(['projects'])
},
async mounted() {
await this.loadProjects()
}
}
</script>
<style scoped>
button {
margin-left: 5px;
}
</style>
\ No newline at end of file
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import "bootstrap/dist/css/bootstrap.min.css"
import "bootstrap"
createApp(App).use(store).use(router).mount('#app')
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
const routes = [
{
path: '/',
name: 'Projects',
component: Home,
},
{
path: '/projects/:id',
name: 'Editor',
component: () => import('../views/Editor.vue'),
props: true
},
{
path: '/login',
name: 'Login',
component: () => import('../views/Login.vue')
},
{
path: '/logout',
name: 'Logout',
component: () => import('../views/Logout.vue')
},
{
path: '/about',
name: 'About',
component: () => import('../views/About.vue')
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
linkActiveClass: "active"
})
router.beforeEach((to, from, next) => {
document.title = `Simple Editor | ${to.name}`;
next();
});
export default router
import { createStore } from 'vuex'
export default createStore({
state: {
counter: 1,
projects: {},
user: null
},
mutations: {
increment(state, amount) {
state.counter += amount
},
decrement(state, amount) {
state.counter -= amount
},
setProjects(state, projects) {
state.projects = projects
},
setUser(state, user) {
state.user = user
}
},
getters: {
isAuthenticated: state => !!state.user
},
actions: {
async login({commit}, auth) {
// Prepare headers & auth
const headers = new Headers();
headers.set('Content-Type', 'application/json');
headers.set('Authorization', auth);
// Prepare request
const url = process.env.VUE_APP_API_URL + "users/";
// Send response
const response = await fetch(url, {
method: 'GET',
headers
});
if (!response.ok) {
// Login failed
commit('setUser', null)
} else {
// Get user object
let response_json = await response.json();
let user_object = response_json.results[0]
user_object.auth = auth
commit('setUser', user_object);
}
},
async loadProjects({commit, state}) {
// Prepare headers & auth
const headers = new Headers();
headers.set('Content-Type', 'application/json');
headers.set('Authorization', state.user.auth);
// Prepare request
const url = process.env.VUE_APP_API_URL + "projects/";
// Send response
const response = await fetch(url, {
method: 'GET',
headers
});
if (!response.ok) {
const message = `An error has occurred: ${response.status}`;
throw new Error(message);
}
let response_json = await response.json();
commit('setProjects', response_json.results);
},
async createProject({state, dispatch}, payload) {
// Prepare headers & auth
const headers = new Headers();
headers.set('Content-Type', 'application/json');
headers.set('Authorization', state.user.auth);
// Prepare request
const url = process.env.VUE_APP_API_URL + "projects/";
// Send response
const response = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify(payload)
});
if (!response.ok) {
const message = `An error has occurred: ${response.status}`;
throw new Error(message);
}
// reload projects
dispatch('loadProjects')
},
async deleteProject({state, dispatch}, id) {
// Prepare headers & auth
const headers = new Headers();
headers.set('Content-Type', 'application/json');
headers.set('Authorization', state.user.auth);
// Prepare request
const url = process.env.VUE_APP_API_URL + `projects/${id}/`;
// Send response
const response = await fetch(url, {
method: 'DELETE',
headers
});
if (!response.ok) {
const message = `An error has occurred: ${response.status}`;
throw new Error(message);
}
// reload projects
dispatch('loadProjects')
}
},
modules: {
}
})
<template>
<div class="container">
<div class="about">
<h1>This is an about page `{{ envVar }}`</h1>
</div>
</div>
</template>
<script>
export default {
data() {
return {
}
},
computed: {
envVar() {
return process.env.VUE_APP_ENV_VARIABLE
},
}
}
</script>
<style scoped>
</style>
\ No newline at end of file
<template>
<div class="row">
<div class="col-sm-12 col-md-4">
<Files/>
<Clips/>
</div>
<div class="col-sm-12 col-md-8">
<Preview/>
</div>
</div>
</template>
<script>
import Files from "../components/Files";
import Clips from "../components/Clips";
import Preview from "../components/Preview";
export default {
name: "Editor.vue",
computed: {
id() {
return this.$route.params.id
}
},
components: {
Files, Clips, Preview
}
}
</script>
<style scoped>
</style>
\ No newline at end of file
<template>
<Projects/>
</template>
<script>
// @ is an alias to /src
import Projects from "../components/Projects";
import { mapGetters } from 'vuex'
export default {
name: 'Home',
components: {
Projects
},
computed: {
...mapGetters(['isAuthenticated'])
},
mounted() {
if (!this.isAuthenticated) {
// login failed, show login page
this.$router.push('/login')
}
}
}
</script>
<template>
<div class="row p-5 justify-content-center">
<div class="col-3">
<div class="mb-3">
<label class="form-label">Username</label>
<input type="text" class="form-control" v-model="username">
</div>
<div class="mb-3">
<label class="form-label">Password</label>
<input type="password" class="form-control" v-model="password">
</div>
<button type="submit" class="btn btn-primary" @click="loginClick">Login</button>
</div>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
export default {
name: "Login.vue",
data() {
return {
username: null,
password: null
}
},
methods: {
async loginClick() {
let hash = btoa(`${this.username}:${this.password}`)
let auth = `Basic ${hash}`
await this.login(auth)
if (this.isAuthenticated) {
localStorage.auth = auth
this.$router.push('/')
}
},
...mapActions(['login'])
},
computed: {
...mapGetters(['isAuthenticated'])
},
}
</script>
<style scoped>
</style>
\ No newline at end of file
<script>
import { mapMutations } from 'vuex'
export default {
name: "Logout.vue",
methods: {
...mapMutations(['setUser','setProjects'])
},
mounted() {
// Logout user
this.setUser(null)
this.setProjects(null)
localStorage.auth = null
this.$router.push('/login')
}
}
</script>
<style scoped>
</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