diff --git a/public/lang/en.json b/public/lang/en.json index 715ff5ca..a7751e48 100644 --- a/public/lang/en.json +++ b/public/lang/en.json @@ -423,6 +423,7 @@ "history": "History", "action": "Action", "category_action": "Category action", + "user_data_overwrite": "Found existing data", "connect": "Connect", "cloud_update": "Syncing with cloud", "cloud_method": "Data location", @@ -565,6 +566,7 @@ "pdf_single_page": "Render as one document", "convert_to_images": "Convert to images", "converting": "Converting...", + "closing": "Closing app...", "remove_template_from_show": "Remove template from show", "reset_defaults": "Reset defaults", "to_all": "Apply to all", @@ -747,6 +749,7 @@ "all_shows": "All shows", "all_projects": "All projects", "project": "Project", + "include_media": "Include media files", "preview": "Preview", "title": "Title", "metadata": "Metadata", @@ -1120,6 +1123,9 @@ "show_location": "Show location", "data_location": "Data location", "user_data_location": "Save user settings at 'Data location'", + "user_data_exists": "Found existing data at custom location, would you like to overwrite it?", + "user_data_yes": "Yes, keep current data", + "user_data_no": "No, import existing data", "popup_before_close": "Enable close confirmation popup", "disable_hardware_acceleration": "Disable hardware acceleration", "restart_for_change": "You have to restart the program for the change to take effect!", diff --git a/src/electron/data/export.ts b/src/electron/data/export.ts index b2b00160..d858683e 100644 --- a/src/electron/data/export.ts +++ b/src/electron/data/export.ts @@ -243,37 +243,46 @@ function exportAllShows(data: any) { // ----- PROJECT ----- export function exportProject(data: any) { - toApp(MAIN, {channel: "ALERT", data: "export.exporting"}) + toApp(MAIN, { channel: "ALERT", data: "export.exporting" }) + + const files = data.file.files || [] + if (!files.length) { + // export as plain JSON + writeFile(join(data.path, data.name), ".project", JSON.stringify(data.file), "utf-8", (err: any) => doneWritingFile(err, data.path)) + return + } // create archive const zip = new AdmZip() // copy files - const files = data.file.files || [] files.forEach((path: string) => { zip.addLocalFile(path) - }); + }) // add project file zip.addFile("data.json", Buffer.from(JSON.stringify(data.file))) - - const outputPath = join(data.path, data.name + ".project") - zip.writeZip(outputPath, (err: any) => doneWritingFile(err, data.path)); - // plain JSON - // writeFile(join(data.path, data.name), ".project", JSON.stringify(data.file), "utf-8", (err: any) => doneWritingFile(err, data.path)) + const outputPath = join(data.path, data.name) + let p = getUniquePath(outputPath, ".project") + zip.writeZip(p, (err: any) => doneWritingFile(err, data.path)) } // ----- HELPERS ----- function writeFile(path: string, extension: string, data: any, options: any = undefined, callback: any) { + let p = getUniquePath(path, extension) + fs.writeFile(p, data, options, callback) +} + +function getUniquePath(path: string, extension: string) { let number = -1 - let tempPath: string = path + let p: string = path do { number++ - tempPath = path + (number ? "_" + number : "") + extension - } while (doesPathExist(tempPath)) + p = path + (number ? "_" + number : "") + extension + } while (doesPathExist(p)) - fs.writeFile(tempPath, data, options, callback) + return p } diff --git a/src/electron/data/thumbnails.ts b/src/electron/data/thumbnails.ts index 751ff905..f98789c7 100644 --- a/src/electron/data/thumbnails.ts +++ b/src/electron/data/thumbnails.ts @@ -1,11 +1,13 @@ -import { NativeImage, ResizeOptions, app, nativeImage } from "electron" +import { BrowserWindow, NativeImage, ResizeOptions, app, nativeImage } from "electron" import fs from "fs" import path from "path" -import { isProd, toApp } from ".." -import { MAIN } from "../../types/Channels" +import { isProd, loadWindowContent, toApp } from ".." +import { MAIN, OUTPUT } from "../../types/Channels" import { doesPathExist, doesPathExistAsync, makeDir } from "../utils/files" import { waitUntilValueIsDefined } from "../utils/helpers" import { imageExtensions, videoExtensions } from "./media" +import { captureOptions } from "../utils/windowOptions" +import { OutputHelper } from "../output/OutputHelper" export function getThumbnail(data: any) { let output = createThumbnail(data.input, data.size || 500) @@ -188,3 +190,33 @@ function saveToDisk(savePath: string, image: NativeImage, nextOnFinished: boolea if (nextOnFinished) generationFinished() }) } + +///// CAPTURE SLIDE ///// + +export function captureSlide(data: any) { + const OUTPUT_ID = "capture" + if (OutputHelper.getOutput(OUTPUT_ID)) return + + let window = new BrowserWindow({ ...captureOptions, width: data.resolution?.width, height: data.resolution?.height }) + loadWindowContent(window, "output") + + OutputHelper.setOutput(OUTPUT_ID, { window }) + + window.on("ready-to-show", () => { + // send correct output data after load + setTimeout(() => { + window.webContents.send(OUTPUT, { channel: "OUTPUTS", data: data.output }) + // WIP mute videos + + // wait for content load + setTimeout(async () => { + const page = await window.capturePage() + const base64 = page.toDataURL({ scaleFactor: 1 }) + toApp(MAIN, { channel: "CAPTURE_SLIDE", data: { listenerId: data.listenerId, base64 } }) + + window.destroy() + OutputHelper.deleteOutput(OUTPUT_ID) + }, 3000) + }, 1000) + }) +} diff --git a/src/electron/index.ts b/src/electron/index.ts index 733e3da8..68403d47 100644 --- a/src/electron/index.ts +++ b/src/electron/index.ts @@ -158,21 +158,22 @@ function createMain() { if (RECORD_STARTUP_TIME) console.timeEnd("Main window") } -export function loadWindowContent(window: BrowserWindow, isOutput: boolean = false) { - if (!isOutput && RECORD_STARTUP_TIME) console.time("Main window content") - if (!isOutput) console.log("Loading main window content") +export function loadWindowContent(window: BrowserWindow, type: null | "output" = null) { + let mainOutput = type === null + if (mainOutput && RECORD_STARTUP_TIME) console.time("Main window content") + if (mainOutput) console.log("Loading main window content") if (isProd) window.loadFile("public/index.html").catch(error) else window.loadURL("http://localhost:3000").catch(error) window.webContents.on("did-finish-load", () => { - if (window === mainWindow) isOutput = false // make sure window is not output - window.webContents.send(STARTUP, { channel: "TYPE", data: isOutput ? "output" : null }) - if (!isOutput) retryLoadingContent() + // if (window === mainWindow) type = null // make sure type is correct + window.webContents.send(STARTUP, { channel: "TYPE", data: type }) + if (mainOutput) retryLoadingContent() }) function error(err: any) { console.error("Failed to load window:", JSON.stringify(err)) - if (isLoaded && !isOutput) app.quit() + if (isLoaded && mainOutput) app.quit() } } diff --git a/src/electron/output/helpers/OutputLifecycle.ts b/src/electron/output/helpers/OutputLifecycle.ts index 18b16cb8..7d492e7d 100644 --- a/src/electron/output/helpers/OutputLifecycle.ts +++ b/src/electron/output/helpers/OutputLifecycle.ts @@ -89,7 +89,7 @@ export class OutputLifecycle { }) // window.setVisibleOnAllWorkspaces(true) - loadWindowContent(window, true) + loadWindowContent(window, "output") this.setWindowListeners(window, { id, name }) // open devtools diff --git a/src/electron/utils/responses.ts b/src/electron/utils/responses.ts index 87bfe75e..335f8082 100644 --- a/src/electron/utils/responses.ts +++ b/src/electron/utils/responses.ts @@ -6,14 +6,14 @@ import { app, BrowserWindow, desktopCapturer, DesktopCapturerSource, Display, sc import { machineIdSync } from "node-machine-id" import os from "os" import path from "path" -import { closeMain, isProd, mainWindow, maximizeMain, setGlobalMenu, toApp } from ".." +import { closeMain, exitApp, isProd, mainWindow, maximizeMain, setGlobalMenu, toApp } from ".." import { BIBLE, MAIN, SHOW } from "../../types/Channels" import { restoreFiles } from "../data/backup" import { downloadMedia } from "../data/downloadMedia" import { importShow } from "../data/import" import { convertPDFToImages } from "../data/pdfToImage" import { config, error_log, stores } from "../data/store" -import { getThumbnail, getThumbnailFolderPath, saveImage } from "../data/thumbnails" +import { captureSlide, getThumbnail, getThumbnailFolderPath, saveImage } from "../data/thumbnails" import { OutputHelper } from "../output/OutputHelper" import { getPresentationApplications, presentationControl, startSlideshow } from "../output/ppt/presentation" import { closeServers, startServers } from "../servers" @@ -23,6 +23,7 @@ import { bundleMediaFiles, checkShowsFolder, dataFolderNames, + doesPathExist, getDataFolder, getDocumentsFolder, getFileInfo, @@ -127,6 +128,7 @@ const mainResponses: any = { MEDIA_CODEC: (data: any) => getMediaCodec(data), DOWNLOAD_MEDIA: (data: any) => downloadMedia(data), MEDIA_BASE64: (data: any) => storeMedia(data), + CAPTURE_SLIDE: (data: any) => captureSlide(data), PDF_TO_IMAGE: (data: any) => convertPDFToImages(data), ACCESS_CAMERA_PERMISSION: () => getPermission("camera"), ACCESS_MICROPHONE_PERMISSION: () => getPermission("microphone"), @@ -162,6 +164,21 @@ const mainResponses: any = { // FILES RESTORE: (data: any) => restoreFiles(data), SYSTEM_OPEN: (data: any) => openSystemFolder(data), + DOES_PATH_EXIST: (data: any) => { + let p = data.path + if (p === "data_config") p = path.join(data.dataPath, dataFolderNames.userData) + return { ...data, exists: doesPathExist(p) } + }, + UPDATE_DATA_PATH: () => { + // updateDataPath({ ...data, load: true }) + let special = stores.SETTINGS.get("special") + special.customUserDataLocation = true + stores.SETTINGS.set("special", special) + + toApp(MAIN, { channel: "ALERT", data: "actions.closing" }) + // let user read message and action finish + setTimeout(exitApp, 2000) + }, LOCATE_MEDIA_FILE: (data: any) => locateMediaFile(data), GET_SIMULAR: (data: any) => getSimularPaths(data), BUNDLE_MEDIA_FILES: (data: any) => bundleMediaFiles(data), diff --git a/src/electron/utils/windowOptions.ts b/src/electron/utils/windowOptions.ts index a590f33a..8635f376 100644 --- a/src/electron/utils/windowOptions.ts +++ b/src/electron/utils/windowOptions.ts @@ -97,14 +97,20 @@ export const exportOptions: BrowserWindowConstructorOptions = { }, } -// export const captureOptions: BrowserWindowConstructorOptions = { -// show: false, -// modal: true, -// frame: false, -// skipTaskbar: true, -// webPreferences: { -// webSecurity: isProd, -// backgroundThrottling: false, -// offscreen: true, -// }, -// } +export const captureOptions: BrowserWindowConstructorOptions = { + show: false, + backgroundColor: "#000000", + frame: false, + skipTaskbar: true, + webPreferences: { + preload: join(__dirname, "..", "preload"), + webSecurity: isProd, + nodeIntegration: !isProd, + contextIsolation: true, + allowRunningInsecureContent: false, + webviewTag: true, + backgroundThrottling: false, + autoplayPolicy: "no-user-gesture-required", + offscreen: true, + }, +} diff --git a/src/frontend/classes/Show.ts b/src/frontend/classes/Show.ts index a089df4a..6681ba3d 100644 --- a/src/frontend/classes/Show.ts +++ b/src/frontend/classes/Show.ts @@ -32,6 +32,7 @@ export class ShowObj implements Show { if (template !== false) { // get template from active show (if it's not default with the "Header" template) if (typeof template !== "string" && get(activeShow)?.id !== "default") template = _show().get("settings.template") || null + else if (template === true) template = "" if (!template && get(templates).default) template = "default" } diff --git a/src/frontend/components/actions/api.ts b/src/frontend/components/actions/api.ts index 31263187..5f4f966a 100644 --- a/src/frontend/components/actions/api.ts +++ b/src/frontend/components/actions/api.ts @@ -4,7 +4,7 @@ import { send } from "../../utils/request" import { updateTransition } from "../../utils/transitions" import { startMetronome } from "../drawer/audio/metronome" import { audioPlaylistNext, clearAudio, startPlaylist, updateVolume } from "../helpers/audio" -import { getThumbnail } from "../helpers/media" +import { getSlideThumbnail, getThumbnail } from "../helpers/media" import { changeStageOutputLayout, displayOutputs, startCamera } from "../helpers/output" import { activateTriggerSync, changeOutputStyle, nextSlideIndividual, playSlideTimers, previousSlideIndividual, randomSlide, selectProjectShow, sendMidi, startAudioStream, startShowSync } from "../helpers/showActions" import { playSlideRecording } from "../helpers/slideRecording" @@ -70,6 +70,7 @@ export type API_id_value = { id: string; value: string } export type API_rearrange = { showId: string; from: number; to: number } export type API_group = { showId: string; groupId: string } export type API_layout = { showId: string; layoutId: string } +export type API_slide_thumbnail = { showId?: string; layoutId?: string; index?: number } export type API_media = { path: string } export type API_scripture = { id: string; reference: string } export type API_toggle = { id: string; value?: boolean } @@ -219,6 +220,7 @@ export const API_ACTIONS = { get_groups: (data: API_id) => getShowGroups(data.id), get_thumbnail: (data: API_media) => getThumbnail(data), + get_slide_thumbnail: (data: API_slide_thumbnail) => getSlideThumbnail(data), get_cleared: () => getClearedState(), } diff --git a/src/frontend/components/draw/Slide.svelte b/src/frontend/components/draw/Slide.svelte index 1adad424..fc87e9d0 100644 --- a/src/frontend/components/draw/Slide.svelte +++ b/src/frontend/components/draw/Slide.svelte @@ -24,8 +24,10 @@ return } - let x = (e.clientX - slide.offsetLeft - (slide.closest(".parent").offsetLeft || 0)) / ratio - let y = (e.clientY - slide.offsetTop - (slide.closest(".parent").offsetTop || 0)) / ratio + let centerElem = slide.closest(".parent")?.closest(".center") + + let x = (e.clientX - slide.offsetLeft - (centerElem?.offsetLeft || 0)) / ratio + let y = (e.clientY - slide.offsetTop - (centerElem?.offsetTop || 0)) / ratio if ($drawTool === "pointer" || $drawTool === "focus") { let size = $drawSettings[$drawTool]?.size diff --git a/src/frontend/components/drawer/media/MediaCard.svelte b/src/frontend/components/drawer/media/MediaCard.svelte index ecbc576b..231c2eb9 100644 --- a/src/frontend/components/drawer/media/MediaCard.svelte +++ b/src/frontend/components/drawer/media/MediaCard.svelte @@ -85,6 +85,8 @@ if (credits.type === "unsplash" && credits.trigger_download) { fetch(credits.trigger_download + "?client_id=" + getKey("unsplash"), { method: "GET" }).catch((err) => console.error("Could not trigger download:", err)) customMessageCredits.set(`Photo by ${credits.artist} on Unsplash`) + } else { + customMessageCredits.set("") } } diff --git a/src/frontend/components/drawer/pages/Variables.svelte b/src/frontend/components/drawer/pages/Variables.svelte index 7c124447..5fc7d66c 100644 --- a/src/frontend/components/drawer/pages/Variables.svelte +++ b/src/frontend/components/drawer/pages/Variables.svelte @@ -30,7 +30,7 @@ {#if sortedVariables.length}
{#each sortedVariables as variable} - +
diff --git a/src/frontend/components/edit/scripts/itemHelpers.ts b/src/frontend/components/edit/scripts/itemHelpers.ts index ec998b5e..ca4417bb 100644 --- a/src/frontend/components/edit/scripts/itemHelpers.ts +++ b/src/frontend/components/edit/scripts/itemHelpers.ts @@ -10,7 +10,7 @@ import { clone, keysToID, sortByName } from "../../helpers/array" export const DEFAULT_ITEM_STYLE = "top:120px;left:50px;height:840px;width:1820px;" -export function addItem(type: ItemType, id: any = null, options: any = {}) { +export function addItem(type: ItemType, id: any = null, options: any = {}, value: string = "") { let activeTemplate: string | null = get(activeShow)?.id ? get(showsCache)[get(activeShow)!.id!]?.settings?.template : null let template = activeTemplate ? get(templates)[activeTemplate]?.items : null @@ -20,7 +20,7 @@ export function addItem(type: ItemType, id: any = null, options: any = {}) { } if (id) newData.id = id - if (type === "text") newData.lines = [{ align: template?.[0]?.lines?.[0]?.align || "", text: [{ value: "", style: template?.[0]?.lines?.[0]?.text?.[0]?.style || "" }] }] + if (type === "text") newData.lines = [{ align: template?.[0]?.lines?.[0]?.align || "", text: [{ value, style: template?.[0]?.lines?.[0]?.text?.[0]?.style || "" }] }] if (type === "list") newData.list = { items: [] } // else if (type === "timer") newData.timer = { id: uid(), name: get(dictionary).timer?.counter || "Counter", type: "counter", start: 300, end: 0 } else if (type === "timer") { diff --git a/src/frontend/components/export/project.ts b/src/frontend/components/export/project.ts index 9ac0db2c..d3ae5a42 100644 --- a/src/frontend/components/export/project.ts +++ b/src/frontend/components/export/project.ts @@ -4,7 +4,7 @@ import { get } from "svelte/store" import { EXPORT } from "../../../types/Channels" import type { Project, ProjectShowRef } from "../../../types/Projects" -import { dataPath, folders, overlays as overlayStores, showsCache } from "../../stores" +import { dataPath, folders, media, overlays as overlayStores, showsCache, special } from "../../stores" import { send } from "../../utils/request" import { clone } from "../helpers/array" import { loadShows } from "../helpers/setShow" @@ -15,7 +15,7 @@ import type { SlideData } from "../../../types/Show" export async function exportProject(project: Project) { let shows: any = {} let files: string[] = [] - let overlays: {[key: string]: any} = {} + let overlays: { [key: string]: any } = {} // get project project = clone(project) @@ -30,8 +30,8 @@ export async function exportProject(project: Project) { let refs = _show(showRef.id).layouts().ref() let mediaIds: string[] = [] - refs.forEach(ref => { - ref.forEach(({data}: {data: SlideData}) => { + refs.forEach((ref) => { + ref.forEach(({ data }: { data: SlideData }) => { // background let background = data.background if (background) mediaIds.push(background) @@ -43,12 +43,12 @@ export async function exportProject(project: Project) { // overlays let overlays = data.overlays || [] overlays.forEach(getOverlay) - }); + }) }) // get media file paths let media = _show(showRef.id).get("media") - mediaIds.forEach(id => { + mediaIds.forEach((id) => { getFile(media[id].path || media[id].id) }) @@ -69,7 +69,7 @@ export async function exportProject(project: Project) { } let projectItems = project.shows - + // load shows let showIds = projectItems.filter((a) => (a.type || "show") === "show").map((a) => a.id) await loadShows(showIds) @@ -79,14 +79,31 @@ export async function exportProject(project: Project) { // remove duplicates files = [...new Set(files)] + // set data + const projectData: any = { project, parentFolder, shows, overlays } + let includeMediaFiles = get(special).projectIncludeMedia ?? true + if (includeMediaFiles) { + projectData.files = files + + let mediaData: any = {} + files.forEach((path) => { + if (!get(media)[path]) return + + let data = clone(get(media)[path]) + // delete data.info + mediaData[path] = data + }) + if (Object.keys(mediaData).length) projectData.media = mediaData + } + // export to file - send(EXPORT, ["GENERATE"], { type: "project", path: get(dataPath), name: formatToFileName(project.name), file: { project, parentFolder, shows, files, overlays } }) + send(EXPORT, ["GENERATE"], { type: "project", path: get(dataPath), name: formatToFileName(project.name), file: projectData }) function getItem(showRef: ProjectShowRef) { let type = showRef.type || "show" if (!getProjectItems[type]) { - console.log("Missing project type:", type); + console.log("Missing project type:", type) return } @@ -102,7 +119,7 @@ export async function exportProject(project: Project) { overlays[id] = clone(get(overlayStores)[id]) } - + // store as base64 ? // let base64: any = await toDataURL(showRef.id) // media[showRef.id] = base64 diff --git a/src/frontend/components/helpers/array.ts b/src/frontend/components/helpers/array.ts index d25d2a0e..dc6c8391 100644 --- a/src/frontend/components/helpers/array.ts +++ b/src/frontend/components/helpers/array.ts @@ -78,7 +78,7 @@ export function sortObjectNumbers(object: {}[], key: string, reverse: boolean = } // sort any object.name by numbers in the front of the string -export function sortByNameAndNumber(array: any[]) { +export function sortByNameAndNumber(array: any[]) { return array.sort((a, b) => { let aName = ((a.quickAccess?.number || "") + " " + a.name || "").trim() let bName = ((b.quickAccess?.number || "") + " " + b.name || "").trim() @@ -110,7 +110,7 @@ export function sortByNameAndNumber(array: any[]) { export function sortFilenames(filenames) { return filenames.sort(({ name: a }, { name: b }) => { // extract name, number, and extension - const regex = /^(.*?)(?:_(\d+))?(\.\w+)?$/ + const regex = /^(.*?)(\d+)?(\.\w+)?$/ // extract parts const [_, nameA, numA, extA] = a.match(regex) || [a, a, null, null] diff --git a/src/frontend/components/helpers/drop.ts b/src/frontend/components/helpers/drop.ts index 4aa8b53d..109040d9 100644 --- a/src/frontend/components/helpers/drop.ts +++ b/src/frontend/components/helpers/drop.ts @@ -14,7 +14,7 @@ const areas: { [key in DropAreas | string]: string[] } = { project: ["show_drawer", "media", "audio", "overlay", "player", "scripture"], overlays: ["slide"], templates: ["slide"], - edit: ["media"], + edit: ["media", "global_timer", "variable"], // media_drawer: ["file"], } const areaChildren: { [key in DropAreas | string]: string[] } = { diff --git a/src/frontend/components/helpers/dropActions.ts b/src/frontend/components/helpers/dropActions.ts index 3a7fe840..247d134b 100644 --- a/src/frontend/components/helpers/dropActions.ts +++ b/src/frontend/components/helpers/dropActions.ts @@ -269,9 +269,17 @@ export const dropActions: any = { }, overlays: ({ drag, drop }: any) => dropActions.templates({ drag, drop }), edit: ({ drag }: any) => { - if (drag.id !== "media" && drag.id !== "files") return - - drag.data.forEach((file: any) => addItem("media", null, { src: file.path || window.api.showFilePath(file) })) + if (drag.id === "media" || drag.id === "files") { + drag.data.forEach((file: any) => addItem("media", null, { src: file.path || window.api.showFilePath(file) })) + } else if (drag.id === "global_timer") { + drag.data.forEach((a: any) => addItem("timer", null, { timer: { id: a.id } })) + } else if (drag.id === "variable") { + drag.data.forEach((a: any) => { + // showActions.ts getNameId() + let name = a.name?.toLowerCase().trim().replaceAll(" ", "_") || "" + addItem("text", null, {}, `{variable_${name}}`) + }) + } }, audio_playlist: ({ drag, drop }: any, h: any) => { h.id = "UPDATE" diff --git a/src/frontend/components/helpers/media.ts b/src/frontend/components/helpers/media.ts index 6994bd50..cf06de97 100644 --- a/src/frontend/components/helpers/media.ts +++ b/src/frontend/components/helpers/media.ts @@ -6,11 +6,13 @@ import { MAIN } from "../../../types/Channels" import type { MediaStyle } from "../../../types/Main" import type { Styles } from "../../../types/Settings" import type { ShowType } from "../../../types/Show" -import { loadedMediaThumbnails, media, tempPath } from "../../stores" +import { loadedMediaThumbnails, media, outputs, tempPath } from "../../stores" import { newToast, wait, waitUntilValueIsDefined } from "../../utils/common" import { awaitRequest, send } from "../../utils/request" -import type { API_media } from "../actions/api" import { audioExtensions, imageExtensions, mediaExtensions, presentationExtensions, videoExtensions } from "../../values/extensions" +import type { API_media, API_slide_thumbnail } from "../actions/api" +import { getActiveOutputs, getResolution } from "./output" +import { clone } from "./array" export function getExtension(path: string): string { if (!path) return "" @@ -61,7 +63,7 @@ export function joinPath(path: string[]): string { // fix for media files with special characters in file name not playing export function encodeFilePath(path: string): string { if (!path) return "" - + // already encoded if (path.match(/%\d+/g) || path.includes("http") || path.includes("data:")) return path @@ -90,6 +92,27 @@ export async function getThumbnail(data: API_media) { return await toDataURL(path) } +export async function getSlideThumbnail(data: API_slide_thumbnail) { + let outputId = getActiveOutputs(get(outputs), false, true, true)[0] + let outSlide = get(outputs)[outputId]?.out?.slide + + if (!data.showId) data.showId = outSlide?.id + if (!data.layoutId) data.layoutId = outSlide?.layout + if (data.index === undefined) data.index = outSlide?.index + + if (!data?.showId) return + + let output = clone(get(outputs)[outputId]) + if (!output.out) output.out = {} + output.out.slide = { id: data.showId, layout: data.layoutId, index: data.index } + + let resolution = getResolution() + resolution = { width: resolution.width * 0.5, height: resolution.height * 0.5 } + + const thumbnail = await awaitRequest(MAIN, "CAPTURE_SLIDE", { output: { [outputId]: output }, resolution }) + return thumbnail.base64 +} + // convert to base64 async function toDataURL(url: string): Promise { return new Promise((resolve: any) => { diff --git a/src/frontend/components/main/popups/UserDataOverwrite.svelte b/src/frontend/components/main/popups/UserDataOverwrite.svelte new file mode 100644 index 00000000..a3e82e83 --- /dev/null +++ b/src/frontend/components/main/popups/UserDataOverwrite.svelte @@ -0,0 +1,59 @@ + + +

+ +
+ + +
+ + diff --git a/src/frontend/components/main/popups/export/Export.svelte b/src/frontend/components/main/popups/export/Export.svelte index 0eeeac5c..61ea1785 100644 --- a/src/frontend/components/main/popups/export/Export.svelte +++ b/src/frontend/components/main/popups/export/Export.svelte @@ -2,7 +2,7 @@ import { EXPORT } from "../../../../../types/Channels" import type { Project } from "../../../../../types/Projects" import { Show } from "../../../../../types/Show" - import { activePopup, activeProject, dataPath, projects, showsCache, showsPath } from "../../../../stores" + import { activePopup, activeProject, dataPath, projects, showsCache, showsPath, special } from "../../../../stores" import { send } from "../../../../utils/request" import { exportProject } from "../../../export/project" import { clone } from "../../../helpers/array" @@ -10,6 +10,7 @@ import { loadShows } from "../../../helpers/setShow" import T from "../../../helpers/T.svelte" import Button from "../../../inputs/Button.svelte" + import Checkbox from "../../../inputs/Checkbox.svelte" import CombinedInput from "../../../inputs/CombinedInput.svelte" import Dropdown from "../../../inputs/Dropdown.svelte" import Center from "../../../system/Center.svelte" @@ -81,6 +82,14 @@ } let pdfOptions: any = {} + + function setSpecial(e: any, key: string) { + let value = e?.target?.checked + special.update((a) => { + a[key] = value + return a + }) + }
@@ -98,6 +107,17 @@
{/if} +{#if format.id === "project"} + +

+
+ setSpecial(e, "projectIncludeMedia")} /> +
+
+ +
+{/if} + {#if loading}
diff --git a/src/frontend/components/output/Output.svelte b/src/frontend/components/output/Output.svelte index af229a7d..110b9f4f 100644 --- a/src/frontend/components/output/Output.svelte +++ b/src/frontend/components/output/Output.svelte @@ -296,7 +296,7 @@ {/if} - {#if currentOutput.active || mirror} + {#if currentOutput.active || (mirror && !preview)} {/if} diff --git a/src/frontend/components/settings/tabs/Other.svelte b/src/frontend/components/settings/tabs/Other.svelte index 48f6c9c4..50f6b2e2 100644 --- a/src/frontend/components/settings/tabs/Other.svelte +++ b/src/frontend/components/settings/tabs/Other.svelte @@ -2,7 +2,7 @@ import { onDestroy, onMount } from "svelte" import { EXPORT, MAIN } from "../../../../types/Channels" import { activePage, activePopup, alertMessage, alertUpdates, dataPath, deletedShows, dictionary, popupData, shows, showsCache, showsPath, special, usageLog } from "../../../stores" - import { destroy, receive, send } from "../../../utils/request" + import { awaitRequest, destroy, receive, send } from "../../../utils/request" import { save } from "../../../utils/save" import Icon from "../../helpers/Icon.svelte" import T from "../../helpers/T.svelte" @@ -41,12 +41,21 @@ const isChecked = (e: any) => e.target.checked - function toggle(e: any, key: string) { + async function toggle(e: any, key: string) { let checked = e.target.checked - updateSpecial(checked, key) if (key === "customUserDataLocation") { - save(false, { backup: true, changeUserData: { reset: !checked, dataPath: $dataPath } }) + let existingData = false + if (checked) { + existingData = (await awaitRequest(MAIN, "DOES_PATH_EXIST", { path: "data_config", dataPath: $dataPath }))?.exists + if (existingData) activePopup.set("user_data_overwrite") + } + if (!existingData) { + updateSpecial(checked, key) + save(false, { backup: true, changeUserData: { reset: !checked, dataPath: $dataPath } }) + } + } else { + updateSpecial(checked, key) } } diff --git a/src/frontend/components/slide/Layouts.svelte b/src/frontend/components/slide/Layouts.svelte index 3947693e..70adb40e 100644 --- a/src/frontend/components/slide/Layouts.svelte +++ b/src/frontend/components/slide/Layouts.svelte @@ -4,6 +4,7 @@ import { activePopup, activeProject, activeShow, alertMessage, dictionary, labelsDisabled, notFound, openToolsTab, projects, showsCache, slidesOptions } from "../../stores" import Icon from "../helpers/Icon.svelte" import T from "../helpers/T.svelte" + import { keysToID, sortByName } from "../helpers/array" import { duplicate } from "../helpers/clipboard" import { history } from "../helpers/history" import { _show } from "../helpers/shows" @@ -13,7 +14,6 @@ import Center from "../system/Center.svelte" import SelectElem from "../system/SelectElem.svelte" import Reference from "./Reference.svelte" - import { keysToID, sortByName } from "../helpers/array" $: showId = $activeShow?.id || "" $: currentShow = $showsCache[showId] || {} diff --git a/src/frontend/components/stage/Stagebox.svelte b/src/frontend/components/stage/Stagebox.svelte index 476371d7..a079792f 100644 --- a/src/frontend/components/stage/Stagebox.svelte +++ b/src/frontend/components/stage/Stagebox.svelte @@ -204,7 +204,7 @@ {/key} {:else if id.includes("clock")} - + {:else if id.includes("video")} {:else if id.includes("first_active_timer")} diff --git a/src/frontend/components/stage/tools/BoxStyle.svelte b/src/frontend/components/stage/tools/BoxStyle.svelte index c16bd2e6..72082fe2 100644 --- a/src/frontend/components/stage/tools/BoxStyle.svelte +++ b/src/frontend/components/stage/tools/BoxStyle.svelte @@ -31,7 +31,8 @@ delete newEdits.align delete newEdits.chords edits = { default: trackerEdits, font: edits.default, ...newEdits } - } else if (items[0].includes("output")) edits = {} + } else if (items[0].includes("clock")) edits.default.push({ name: "clock.seconds", id: "clock.seconds", input: "checkbox", value: true }) + else if (items[0].includes("output")) edits = {} } let data: { [key: string]: any } = {} diff --git a/src/frontend/converters/project.ts b/src/frontend/converters/project.ts index 35f04ed1..5cc16f40 100644 --- a/src/frontend/converters/project.ts +++ b/src/frontend/converters/project.ts @@ -3,18 +3,18 @@ import type { ShowType } from "../../types/Show" import { history } from "../components/helpers/history" import { getExtension, getFileName, getMediaType, removeExtension } from "../components/helpers/media" import { checkName } from "../components/helpers/show" -import { activeProject, activeShow, folders, projects, overlays as overlayStores, alertMessage, activePopup } from "../stores" +import { activeProject, activeShow, folders, projects, overlays as overlayStores, media as mediaStores, alertMessage, activePopup } from "../stores" export function importProject(files: any) { files.forEach(({ content }: any) => { - let { project, parentFolder, shows, overlays } = JSON.parse(content) + let { project, parentFolder, shows, overlays, media } = JSON.parse(content) // find any parent folder with the same name as previous parent, or place at root if (parentFolder) project.parent = Object.entries(get(folders)).find(([_id, folder]: any) => folder.name === parentFolder)?.[0] || "/" // add overlays if (overlays) { - overlayStores.update(a => { + overlayStores.update((a) => { Object.entries(overlays).forEach(([id, overlay]: any) => { // create new or replace existing a[id] = overlay @@ -23,6 +23,16 @@ export function importProject(files: any) { }) } + // get media data + if (media) { + mediaStores.update((a) => { + Object.entries(media).forEach(([path, data]: any) => { + a[path] = { ...(a[path] || {}), ...data } + }) + return a + }) + } + // create shows let newShows: any[] = [] Object.entries(shows).forEach(([id, show]: any) => { diff --git a/src/frontend/utils/listeners.ts b/src/frontend/utils/listeners.ts index 11de9fd6..47a97418 100644 --- a/src/frontend/utils/listeners.ts +++ b/src/frontend/utils/listeners.ts @@ -161,12 +161,13 @@ export function storeSubscriber() { }) draw.subscribe((data) => { - let activeOutputs = getActiveOutputs() + let activeOutputs = getActiveOutputs(get(outputs), true, true, true) activeOutputs.forEach((id) => { send(OUTPUT, ["DRAW"], { id, data }) }) }) drawTool.subscribe((data) => { + // WIP changing tool while output is not active, will not update tool in output if set to active before changing tool again let activeOutputs = getActiveOutputs() activeOutputs.forEach((id) => { send(OUTPUT, ["DRAW_TOOL"], { id, data }) diff --git a/src/frontend/utils/popup.ts b/src/frontend/utils/popup.ts index d29c2ce1..34200420 100644 --- a/src/frontend/utils/popup.ts +++ b/src/frontend/utils/popup.ts @@ -47,6 +47,7 @@ import Trigger from "../components/main/popups/Trigger.svelte" import Unsaved from "../components/main/popups/Unsaved.svelte" import Variable from "../components/main/popups/Variable.svelte" import { activePopup, popupData } from "../stores" +import UserDataOverwrite from "../components/main/popups/UserDataOverwrite.svelte" export const popups: { [key in Popups]: ComponentType } = { initialize: Initialize, @@ -91,6 +92,7 @@ export const popups: { [key in Popups]: ComponentType } = { history: History, action: Action, category_action: CategoryAction, + user_data_overwrite: UserDataOverwrite, connect: Connect, cloud_update: CloudUpdate, cloud_method: CloudMethod, diff --git a/src/server/stage/components/Stagebox.svelte b/src/server/stage/components/Stagebox.svelte index 46ff1c20..54aa0a62 100644 --- a/src/server/stage/components/Stagebox.svelte +++ b/src/server/stage/components/Stagebox.svelte @@ -140,7 +140,7 @@ {:else if id.includes("clock")} - + {:else if id.includes("video")} {:else if id.includes("first_active_timer")} diff --git a/src/types/Main.ts b/src/types/Main.ts index 0519433a..c5e9adcc 100644 --- a/src/types/Main.ts +++ b/src/types/Main.ts @@ -153,6 +153,7 @@ export type Popups = | "history" | "action" | "category_action" + | "user_data_overwrite" | "connect" | "cloud_update" | "cloud_method"